///|
fn validate_key(key : String) -> Unit raise FlacError {
guard !key.is_empty() else { raise Invalid(0, "empty comment key") }
for c in key.code_units() {
guard c.to_int() >= 0x20 && c.to_int() <= 0x7d && c.to_int() != 0x3d else {
raise Invalid(0, "comment key must be ASCII 0x20..0x7d except equals")
}
}
}
///|
pub fn decode_comments(data : Bytes) -> Comments raise FlacError {
check_block_size(data.length())
let r = { data, pos: 0, }
let vendor = text(r.sized(little=true))
let count = r.u32(little=true)
// Each entry requires at least four length bytes. Check before allocating/looping.
guard count <= ((data.length() - r.pos) / 4).reinterpret_as_uint() else {
raise Invalid(r.pos, "comment count exceeds block")
}
let entries : Array[Comment] = []
for _ in 0.. Bytes raise FlacError {
let out = @buffer.Buffer()
put_sized(out, text_bytes(comments.vendor), little=true)
out.write_uint_le(comments.entries.length().reinterpret_as_uint())
for entry in comments.entries {
validate_key(entry.key)
let data = text_bytes("\{entry.key}=\{entry.value}")
guard data.length() <= MaxBlock - out.length() - 4 else {
raise Limit("comments exceed metadata block limit")
}
put_sized(out, data, little=true)
}
check_block_size(out.length())
out.to_bytes()
}
///|
/// Preserve duplicate keys and their order; matching is ASCII case-insensitive.
pub fn Comments::values(self : Comments, key : String) -> Array[String] {
self.entries.filter(e => e.key.equal_ignore_ascii_case(key)).map(e => e.value)
}
///|
fn edit_comments(
comments : Comments,
edits : Array[TagEdit],
) -> Comments raise FlacError {
let mut entries = comments.entries.copy()
for edit in edits {
match edit {
Set(key, value) => {
validate_key(key)
entries = entries.filter(e => !e.key.equal_ignore_ascii_case(key))
entries.push({ key, value, })
}
Add(key, value) => {
validate_key(key)
entries.push({ key, value, })
}
Remove(key) => {
validate_key(key)
entries = entries.filter(e => !e.key.equal_ignore_ascii_case(key))
}
}
}
{ vendor: comments.vendor, entries, }
}