///|
priv struct Writer {
out : StringBuilder
limits : Limits
mut size : Int32
mut nodes : Int32
}
///|
fn Writer::emit(self : Writer, text : String) -> Unit raise ConfigError {
if text.length() > self.limits.max_input - self.size {
raise failure("output_limit", "normalized output exceeds byte limit")
}
self.size += text.length()
self.out.write_string(text)
}
///| Write a root group as deterministic ASCII libconfig text.
///| All non-ASCII bytes are escaped; comments and numeric bases are not preserved.
///|
/// max_input bounds the serialized output; all other limits also apply.
pub fn stringify(
root : Value,
limits~ : Limits = Limits::default(),
) -> String raise ConfigError {
ignore(Reader::new(b"", limits))
guard root is Group(items) else {
raise failure("type_mismatch", "configuration root must be a group")
}
let w : Writer = { out: StringBuilder(), limits, size: 0, nodes: 0, }
w.settings(items, 0)
w.out.to_string()
}
///|
fn Writer::settings(
self : Writer,
items : Array[(String, Value)],
depth : Int32,
) -> Unit raise ConfigError {
let seen : Map[String, Bool] = Map([])
for (name, value) in items {
check_name(name)
if seen.contains(name) {
raise failure("duplicate_name", "duplicate setting: " + name)
}
seen[name] = true
self.emit(name)
self.emit(" = ")
self.value(value, depth)
self.emit(";\n")
}
}
///|
fn Writer::value(
self : Writer,
value : Value,
depth : Int32,
) -> Unit raise ConfigError {
if self.nodes >= self.limits.max_nodes {
raise failure("node_limit", "value count exceeds limit")
}
self.nodes += 1
match value {
Integer(v) => self.emit(v.to_string())
Integer64(v) => {
self.emit(v.to_string())
self.emit("L")
}
Float(v) => {
if v.is_nan() || v.is_inf() {
raise failure("non_finite_float", "cannot serialize non-finite float")
}
let text = if v == 0.0 && v.reinterpret_as_int64() < 0L {
"-0.0"
} else {
v.to_string()
}
self.emit(text)
if !text.contains(".") && !text.contains("e") && !text.contains("E") {
self.emit(".0")
}
}
Boolean(v) => self.emit(if v { "true" } else { "false" })
Text(bytes) => {
if bytes.length() > self.limits.max_string {
raise failure("string_limit", "string exceeds byte limit")
}
self.emit("\"")
for b in bytes {
let c = b.to_int()
if c == 0 {
raise failure(
"unsupported_nul", "NUL bytes are not representable by libconfig string APIs",
)
}
if c == 34 {
self.emit("\\\"")
} else if c == 92 {
self.emit("\\\\")
} else if c >= 32 && c <= 126 {
self.emit(c.unsafe_to_char().to_string())
} else {
let digits = "0123456789ABCDEF"
self.emit("\\x")
self.emit(digits[c / 16:c / 16 + 1].to_owned())
self.emit(digits[c % 16:c % 16 + 1].to_owned())
}
}
self.emit("\"")
}
Group(items) => {
self.check_depth(depth)
self.emit("{\n")
self.settings(items, depth + 1)
self.emit("}")
}
ScalarArray(items) | List(items) => {
self.check_depth(depth)
let array = value is ScalarArray(_)
self.emit(if array { "[" } else { "(" })
for i = 0; i < items.length(); i = i + 1 {
if array {
match items[i] {
Group(_) | ScalarArray(_) | List(_) =>
raise failure("array_type", "array elements must be scalars")
_ => ()
}
if items[i].kind() != items[0].kind() {
raise failure("array_type", "mixed scalar array")
}
}
if i > 0 {
self.emit(", ")
}
self.value(items[i], depth + 1)
}
self.emit(if array { "]" } else { ")" })
}
}
}
///|
fn Writer::check_depth(self : Writer, depth : Int32) -> Unit raise ConfigError {
if depth >= self.limits.max_depth {
raise failure("depth_limit", "container nesting exceeds limit")
}
}