///|
/// Current version marker for the portable dictionary text format.
pub const DICTIONARY_FORMAT_VERSION : String = "MLX1"
///|
/// Statistics about a serialized dictionary payload.
pub(all) struct SerializationStats {
version : String
entry_count : Int
byte_length : Int
checksum : Int
valid_header : Bool
} derive(Debug, Eq, ToJson)
///|
/// Escape a dictionary field for the MLX1 line format.
pub fn escape_dictionary_field(value : String) -> String {
let output = StringBuilder()
for ch in value {
match ch {
'\\' => output.write_string("\\\\")
'\t' => output.write_string("\\t")
'\n' => output.write_string("\\n")
'\r' => output.write_string("\\r")
_ => output.write_char(ch)
}
}
output.to_string()
}
///|
/// Decode one escaped MLX1 field. Unknown escapes retain their character.
pub fn unescape_dictionary_field(value : String) -> String {
let output = StringBuilder()
let mut escaping = false
for ch in value {
if escaping {
match ch {
'\\' => output.write_char('\\')
't' => output.write_char('\t')
'n' => output.write_char('\n')
'r' => output.write_char('\r')
_ => {
output.write_char('\\')
output.write_char(ch)
}
}
escaping = false
} else if ch == '\\' {
escaping = true
} else {
output.write_char(ch)
}
}
if escaping {
output.write_char('\\')
}
output.to_string()
}
///|
/// Return a deterministic, line-oriented representation of dictionary entries.
pub fn encode_entries(entries : ArrayView[Entry]) -> String {
let output = StringBuilder()
output.write_string("\{DICTIONARY_FORMAT_VERSION}\n")
for item in entries {
output.write_string(escape_dictionary_field(item.keyword))
output.write_char('\t')
output.write_string(escape_dictionary_field(item.label))
output.write_char('\t')
output.write_object(item.weight)
output.write_char('\n')
}
output.to_string()
}
///|
/// Serialize all entries currently stored in a lexicon.
pub fn serialize_lexicon(lexicon : Lexicon) -> String {
encode_entries(lexicon.entries())
}
///|
/// Decode a MLX1 payload. Malformed rows are skipped and never become empty
/// keywords, which makes it safe to inspect partially written cache files.
pub fn decode_entries(payload : String) -> Array[Entry] {
let lines = [ for line in payload.split("\n") => line.to_owned() ]
if lines.length() == 0 || lines[0] != DICTIONARY_FORMAT_VERSION {
return []
}
let entries : Array[Entry] = []
for index, line in lines {
if index > 0 && line.length() > 0 {
let fields = [ for field in line.split("\t") => field.to_owned() ]
if fields.length() == 3 {
let keyword = unescape_dictionary_field(fields[0])
let label = unescape_dictionary_field(fields[1])
match parse_serialized_int(fields[2]) {
Some(weight) =>
if keyword.length() > 0 {
entries.push({ keyword, label, weight })
}
None => ()
}
}
}
}
entries
}
///|
/// True when a payload has the MLX1 header.
pub fn is_dictionary_payload(payload : String) -> Bool {
let lines = [ for line in payload.split("\n") => line.to_owned() ]
match lines.get(0) {
Some(header) => header == DICTIONARY_FORMAT_VERSION
None => false
}
}
///|
/// Compute a stable non-cryptographic checksum for dictionary entries.
pub fn dictionary_checksum(entries : ArrayView[Entry]) -> Int {
let mut checksum = 216613626
for item in entries {
for ch in item.keyword {
checksum = checksum * 16777619 + ch.to_int()
}
checksum = checksum * 16777619 + 31
for ch in item.label {
checksum = checksum * 16777619 + ch.to_int()
}
checksum = checksum * 16777619 + item.weight
}
if checksum == 0 {
1
} else {
checksum
}
}
///|
/// Compute payload metadata without decoding the dictionary twice.
pub fn serialization_stats(payload : String) -> SerializationStats {
let entries = decode_entries(payload)
let lines = [ for line in payload.split("\n") => line.to_owned() ]
let version = match lines.get(0) {
Some(header) => header
None => ""
}
{
version,
entry_count: entries.length(),
byte_length: payload.length(),
checksum: dictionary_checksum(entries),
valid_header: version == DICTIONARY_FORMAT_VERSION,
}
}
///|
/// Serialize entries and append a checksum trailer for cache validation.
pub fn encode_entries_checked(entries : ArrayView[Entry]) -> String {
let output = StringBuilder()
output.write_string(encode_entries(entries))
output.write_string("# checksum\t")
output.write_object(dictionary_checksum(entries))
output.write_char('\n')
output.to_string()
}
///|
/// Decode a checked payload only when its checksum agrees with the rows.
pub fn decode_entries_checked(payload : String) -> Array[Entry] {
let lines = [ for line in payload.split("\n") => line.to_owned() ]
if lines.length() < 2 || lines[0] != DICTIONARY_FORMAT_VERSION {
return []
}
let trailer = lines[lines.length() - 2]
if !trailer.has_prefix("# checksum\t") {
return []
}
let trailer_fields = [ for field in trailer.split("\t") => field.to_owned() ]
if trailer_fields.length() != 2 {
return []
}
let expected_text = trailer_fields[1]
match parse_serialized_int(expected_text) {
Some(expected) => {
let body = StringBuilder()
for index, line in lines {
if index < lines.length() - 2 {
body.write_string(line)
body.write_char('\n')
}
}
let entries = decode_entries(body.to_string())
if dictionary_checksum(entries) == expected {
entries
} else {
[]
}
}
None => []
}
}
///|
/// Return the number of escaped control characters in a field.
pub fn escaped_character_count(value : String) -> Int {
let mut count = 0
for ch in value {
if ch == '\\' || ch == '\t' || ch == '\n' || ch == '\r' {
count += 1
}
}
count
}
///|
/// Estimate the encoded size of entries without allocating the payload.
pub fn encoded_size(entries : ArrayView[Entry]) -> Int {
let mut size = DICTIONARY_FORMAT_VERSION.length() + 1
for item in entries {
size += item.keyword.length() + escaped_character_count(item.keyword)
size += item.label.length() + escaped_character_count(item.label)
size += 3
size += integer_width(item.weight)
}
size
}
///|
/// Return the integer width used by the text serializer.
pub fn integer_width(value : Int) -> Int {
let mut number = if value < 0 { -value } else { value }
let mut width = if value < 0 { 1 } else { 0 }
if number == 0 {
return width + 1
}
while number > 0 {
width += 1
number /= 10
}
width
}
///|
/// Compare the encoded size estimate with the actual payload length.
pub fn encoded_size_matches(entries : ArrayView[Entry]) -> Bool {
encoded_size(entries) == encode_entries(entries).length()
}
///|
/// Return a copy of entries after a serialize/decode cycle.
pub fn normalize_serialized_entries(entries : ArrayView[Entry]) -> Array[Entry] {
decode_entries(encode_entries(entries))
}
///|
/// Return a short format description for CLI help and diagnostics.
pub fn dictionary_format_description() -> String {
"MLX1: escaped UTF-8 keyword, label, and signed weight rows"
}
///|
fn parse_serialized_int(value : String) -> Int? {
if value.length() == 0 {
return None
}
let chars = value.to_array()
let mut sign = 1
let mut index = 0
if chars[0] == '-' {
sign = -1
index = 1
}
if index >= chars.length() {
return None
}
let mut number = 0
while index < chars.length() {
let ch = chars[index]
if ch < '0' || ch > '9' {
return None
}
number = number * 10 + ch.to_int() - '0'.to_int()
index += 1
}
Some(sign * number)
}