// vorbis_comment.mbt
//
// Vorbis Comment header(每个 Vorbis 流的第二个 packet)解析。
// 包含 vendor 字符串与若干 "KEY=value" 形式的元数据(如 TITLE、ARTIST)。
///|
/// Vorbis comment 元数据。
pub struct VorbisComment {
vendor : String
comments : Array[String]
}
///|
/// 按 key 取第一个匹配的 comment 值,找不到返回 `None`。
///
/// key 按 Vorbis 规范不区分 ASCII 大小写,`"title"` 与 `"TITLE"` 等价。
/// 同一个 key 出现多次时(规范允许,例如多个 ARTIST)返回文件中靠前的那个;
/// 需要全部取值时用 `get_all`。
pub fn VorbisComment::get(self : VorbisComment, key : String) -> String? {
for tag in self.comments {
match split_tag(tag) {
Some((name, value)) =>
if name.equal_ignore_ascii_case(key) {
return Some(value)
}
None => ()
}
}
None
}
///|
/// 按 key 取全部匹配的 comment 值,保持它们在文件中出现的顺序。
///
/// 规范允许同一 key 重复出现(如多位 ARTIST),这里不做去重也不做合并。
pub fn VorbisComment::get_all(
self : VorbisComment,
key : String,
) -> Array[String] {
let out : Array[String] = []
for tag in self.comments {
match split_tag(tag) {
Some((name, value)) =>
if name.equal_ignore_ascii_case(key) {
out.push(value)
}
None => ()
}
}
out
}
///|
/// 把 `"KEY=value"` 按第一个 `=` 拆成 key 与 value。
///
/// 规范要求 key 由 ASCII 可打印字符组成且不含 `=`,因此按 code unit 扫 `=` 是安全的:
/// `=` 是 ASCII,切分点必定落在码元边界上,不会切开代理对。
/// 整条 comment 不含 `=` 时返回 `None`(规范不允许,但畸形流里可能出现)。
fn split_tag(tag : String) -> (String, String)? {
match tag.find("=") {
Some(i) => Some((tag[:i].to_owned(), tag[i + 1:].to_owned()))
None => None
}
}
///|
/// 解析 Comment header packet,返回 vendor 与 comment 列表。
///
/// 字符串按 UTF-8 解码(`@utf8.decode_lossy`),无效字节替换为 U+FFFD,
/// 与 Vorbis comment 的 UTF-8 编码约定相符。
pub fn VorbisComment::parse(packet : Bytes) -> Result[VorbisComment, String] {
if packet.length() < 11 {
return Err("truncated Vorbis comment header")
}
// magic: \x03 "vorbis"
if packet.unsafe_get(0) != b'\x03' ||
packet.unsafe_get(1) != b'v' ||
packet.unsafe_get(2) != b'o' ||
packet.unsafe_get(3) != b'r' ||
packet.unsafe_get(4) != b'b' ||
packet.unsafe_get(5) != b'i' ||
packet.unsafe_get(6) != b's' {
return Err("not a Vorbis comment header")
}
let mut pos = 7
// vendor 字符串
let vendor_len = packet.unsafe_read_uint32_le(pos).reinterpret_as_int()
pos += 4
if vendor_len < 0 || packet.length() < pos + vendor_len {
return Err("truncated vendor string")
}
let vendor = @utf8.decode_lossy(
packet.exact_view(start=pos, end=pos + vendor_len),
)
pos += vendor_len
// comment 数量
if packet.length() < pos + 4 {
return Err("truncated comment count")
}
let n_comments = packet.unsafe_read_uint32_le(pos).reinterpret_as_int()
pos += 4
if n_comments < 0 {
return Err("invalid comment count")
}
// 逐个 comment:4 字节长度 + UTF-8 字符串
let comments : Array[String] = []
for _ in 0..