///|
fn json_hex_digit(value : Int32) -> Char {
match value {
0 => '0'
1 => '1'
2 => '2'
3 => '3'
4 => '4'
5 => '5'
6 => '6'
7 => '7'
8 => '8'
9 => '9'
10 => 'a'
11 => 'b'
12 => 'c'
13 => 'd'
14 => 'e'
_ => 'f'
}
}
///|
/// Quote a string as a JSON value, including control characters and Unicode.
pub fn json_string(value : String) -> String {
let output = StringBuilder()
output.write_char('"')
for character in value.to_array() {
match character {
'"' => output.write_string("\\\"")
'\\' => output.write_string("\\\\")
'\n' => output.write_string("\\n")
'\r' => output.write_string("\\r")
'\t' => output.write_string("\\t")
'\u{0008}' => output.write_string("\\b")
'\u{000c}' => output.write_string("\\f")
_ => {
let code = character.to_int()
if code < 0x20 {
output.write_string("\\u00")
output.write_char(json_hex_digit((code >> 4) & 15))
output.write_char(json_hex_digit(code & 15))
} else {
output.write_char(character)
}
}
}
}
output.write_char('"')
output.to_string()
}
///|
fn optional_json_string(value : String?) -> String {
match value {
Some(text) => json_string(text)
None => "null"
}
}
///|
fn optional_json_integer(value : Int32?) -> String {
match value {
Some(number) => number.to_string()
None => "null"
}
}
///|
fn image_info_json(info : ImageInfo?) -> String {
match info {
Some(image) =>
"{\"width\":" +
image.width.to_string() +
",\"height\":" +
image.height.to_string() +
",\"luminance\":" +
image.luminance.to_string() +
",\"red\":" +
image.red.to_string() +
",\"green\":" +
image.green.to_string() +
",\"blue\":" +
image.blue.to_string() +
"}"
None => "null"
}
}
///|
/// Export the full decision table as a UTF-8 JSON array. This is a report
/// artifact; it is not an approval file and cannot be passed to `apply`.
pub fn review_rows_json(rows : Array[ReviewRow]) -> String {
let output = StringBuilder()
output.write_char('[')
for index = 0; index < rows.length(); index = index + 1 {
if index > 0 {
output.write_char(',')
}
let row = rows[index]
output.write_string("{\"path\":")
output.write_string(json_string(row.path))
output.write_string(",\"name\":")
output.write_string(json_string(row.name))
output.write_string(",\"digest\":")
output.write_string(json_string(row.digest))
output.write_string(",\"kind\":")
output.write_string(json_string(row.kind.code()))
output.write_string(",\"matchPath\":")
output.write_string(optional_json_string(row.match_path))
output.write_string(",\"similarityDistance\":")
output.write_string(optional_json_integer(row.similarity_distance))
output.write_string(",\"destination\":")
output.write_string(optional_json_string(row.destination))
output.write_string(",\"size\":")
output.write_string(row.size.to_string())
output.write_string(",\"imageInfo\":")
output.write_string(image_info_json(row.image_info))
output.write_char('}')
}
output.write_char(']')
output.to_string()
}
///|
fn starts_formula(value : String) -> Bool {
let characters = value.to_array()
let mut index = 0
while index < characters.length() &&
(
characters[index] == ' ' ||
characters[index] == '\t' ||
characters[index] == '\r' ||
characters[index] == '\n'
) {
index = index + 1
}
if index >= characters.length() {
return false
}
match characters[index] {
'=' | '+' | '-' | '@' => true
_ => false
}
}
///|
/// Quote every CSV field so commas, quotes, line breaks and spreadsheet text
/// are preserved as data. Formula-like text receives a leading apostrophe for
/// safer opening in spreadsheet software; JSON remains the exact-value export.
pub fn csv_field(value : String) -> String {
let output = StringBuilder()
output.write_char('"')
if starts_formula(value) {
output.write_char('\'')
}
for character in value.to_array() {
if character == '"' {
output.write_string("\"\"")
} else {
output.write_char(character)
}
}
output.write_char('"')
output.to_string()
}
///|
fn optional_csv(value : String?) -> String {
match value {
Some(text) => text
None => ""
}
}
///|
fn row_csv(row : ReviewRow) -> String {
let fields = [
row.path,
row.name,
row.digest,
row.kind.code(),
optional_csv(row.match_path),
optional_csv(row.similarity_distance.map(x => x.to_string())),
optional_csv(row.destination),
row.size.to_string(),
optional_csv(row.image_info.map(x => image_info_label(x))),
]
let output = StringBuilder()
for index = 0; index < fields.length(); index = index + 1 {
if index > 0 {
output.write_char(',')
}
output.write_string(csv_field(fields[index]))
}
output.to_string()
}
///|
/// Export decisions as RFC-style quoted CSV for spreadsheet review.
pub fn review_rows_csv(rows : Array[ReviewRow]) -> String {
let output = StringBuilder()
output.write_string(
"path,name,sha256,kind,match_path,similarity_distance,destination,size_bytes,image_info\r\n",
)
for row in rows {
output.write_string(row_csv(row))
output.write_string("\r\n")
}
output.to_string()
}
///|
/// Export a concise scan inventory as JSON for scripts and audit tools.
pub fn inventory_json(
summary : InventorySummary,
extensions : Array[ExtensionStats],
size_bands : Array[SizeBandStats],
) -> String {
let output = StringBuilder()
output.write_string("{\"total\":")
output.write_string(summary.total.to_string())
output.write_string(",\"readable\":")
output.write_string(summary.readable.to_string())
output.write_string(",\"unreadable\":")
output.write_string(summary.unreadable.to_string())
output.write_string(",\"bytes\":")
output.write_string(summary.bytes.to_string())
output.write_string(",\"averageBytes\":")
output.write_string(summary.average_size.to_string())
output.write_string(",\"dimensions\":{\"landscape\":")
output.write_string(summary.landscape.to_string())
output.write_string(",\"portrait\":")
output.write_string(summary.portrait.to_string())
output.write_string(",\"square\":")
output.write_string(summary.square.to_string())
output.write_string(",\"unknown\":")
output.write_string(summary.unknown_dimensions.to_string())
output.write_string("},\"pixels\":")
output.write_string(summary.pixels.to_string())
output.write_string(",\"duplicateFileGroups\":")
output.write_string(summary.duplicate_file_groups.to_string())
output.write_string(",\"duplicateExtraFiles\":")
output.write_string(summary.duplicate_files.to_string())
output.write_string(",\"extensions\":[")
for index = 0; index < extensions.length(); index = index + 1 {
if index > 0 {
output.write_char(',')
}
let item = extensions[index]
output.write_string("{\"extension\":")
output.write_string(json_string(item.extension))
output.write_string(",\"count\":")
output.write_string(item.count.to_string())
output.write_string(",\"readable\":")
output.write_string(item.readable.to_string())
output.write_string(",\"unreadable\":")
output.write_string(item.unreadable.to_string())
output.write_string(",\"bytes\":")
output.write_string(item.bytes.to_string())
output.write_char('}')
}
output.write_string("],\"sizeBands\":[")
for index = 0; index < size_bands.length(); index = index + 1 {
if index > 0 {
output.write_char(',')
}
let band = size_bands[index]
output.write_string("{\"label\":")
output.write_string(json_string(band.band.label()))
output.write_string(",\"count\":")
output.write_string(band.count.to_string())
output.write_string(",\"bytes\":")
output.write_string(band.bytes.to_string())
output.write_char('}')
}
output.write_string("]}")
output.to_string()
}
///|
/// Export aggregate extension totals as a spreadsheet-friendly CSV.
pub fn extension_summary_csv(items : Array[ExtensionStats]) -> String {
let output = StringBuilder()
output.write_string("extension,count,readable,unreadable,bytes\r\n")
for item in items {
output.write_string(csv_field(item.extension))
output.write_char(',')
output.write_string(item.count.to_string())
output.write_char(',')
output.write_string(item.readable.to_string())
output.write_char(',')
output.write_string(item.unreadable.to_string())
output.write_char(',')
output.write_string(item.bytes.to_string())
output.write_string("\r\n")
}
output.to_string()
}