1use std::path::{Component, Path, PathBuf};
2
3pub use kcode_k1_audio_classification_format_error::FormatError;
4pub use kcode_k1_transaction_id::TxId;
5pub use kcode_speaker_v3_analysis::{FeatureVector24, LocalSpeakerLabel};
6
7const FRAGMENT_VERSION: u8 = 1;
8const STAGED_KIND: u8 = 1;
9const FINAL_KIND: u8 = 2;
10const ANALYSIS_SLOT_OFFSET: usize = 16;
11const CONFIRMATION_SLOT_OFFSET: usize = 32;
12const BODY_OFFSET: usize = 48;
13const BASE64_ALPHABET: &[u8; 64] =
14 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
15
16#[derive(Debug, Clone, PartialEq)]
17pub struct StagedSpeakerV1 {
18 pub speaker: LocalSpeakerLabel,
19 pub language: String,
20 pub features: FeatureVector24,
21 pub usable_for_training: bool,
22}
23
24#[derive(Debug, Clone, PartialEq)]
25pub struct StagedFragmentV1 {
26 pub analysis_txid: TxId,
27 pub transcript: String,
28 pub speakers: Vec<StagedSpeakerV1>,
29}
30
31#[derive(Debug, Clone, PartialEq)]
32pub struct FinalSpeakerV1 {
33 pub speaker: LocalSpeakerLabel,
34 pub person_id: String,
35 pub language: String,
36 pub features: FeatureVector24,
37 pub usable_for_training: bool,
38}
39
40#[derive(Debug, Clone, PartialEq)]
41pub struct FinalFragmentV1 {
42 pub analysis_txid: TxId,
43 pub confirmation_txid: TxId,
44 pub transcript: String,
45 pub speakers: Vec<FinalSpeakerV1>,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub struct TxIdSlot {
50 txid: TxId,
51}
52
53impl TxIdSlot {
54 pub const LEN: usize = 16;
55 pub const PADDING_LEN: usize = 4;
56
57 pub const fn new(txid: TxId) -> Self {
58 Self { txid }
59 }
60
61 pub const fn txid(self) -> TxId {
62 self.txid
63 }
64
65 pub fn encode(self) -> [u8; Self::LEN] {
66 let mut output = [0; Self::LEN];
67 output[..12].copy_from_slice(self.txid.as_bytes());
68 output
69 }
70
71 pub fn decode(bytes: &[u8]) -> Result<Self, FormatError> {
72 if bytes.len() != Self::LEN {
73 return Err(FormatError::InvalidTxIdSlotLength(bytes.len()));
74 }
75 if bytes[12..].iter().any(|byte| *byte != 0) {
76 return Err(FormatError::NonZeroPadding);
77 }
78 let mut txid = [0; 12];
79 txid.copy_from_slice(&bytes[..12]);
80 Ok(Self::new(TxId::from_bytes(txid)))
81 }
82}
83
84pub fn encode_staged_fragment(value: &StagedFragmentV1) -> Result<Vec<u8>, FormatError> {
85 let mut output = encode_fragment_prefix(STAGED_KIND, value.analysis_txid, None);
86 append_string(&mut output, &value.transcript)?;
87 append_length(&mut output, value.speakers.len())?;
88 for speaker in &value.speakers {
89 append_string(&mut output, &speaker.speaker.to_string())?;
90 append_string(&mut output, &speaker.language)?;
91 append_features(&mut output, &speaker.features)?;
92 output.push(u8::from(speaker.usable_for_training));
93 }
94 Ok(output)
95}
96
97pub fn decode_staged_fragment(bytes: &[u8]) -> Result<StagedFragmentV1, FormatError> {
98 let (analysis_txid, _) = decode_fragment_header(bytes, STAGED_KIND, false)?;
99 let mut decoder = BodyDecoder::new(&bytes[BODY_OFFSET..]);
100 let transcript = decoder.read_string()?;
101 let speaker_count = decoder.read_length()?;
102 let mut speakers = Vec::new();
103 for _ in 0..speaker_count {
104 speakers.push(StagedSpeakerV1 {
105 speaker: decoder.read_speaker_label()?,
106 language: decoder.read_string()?,
107 features: decoder.read_features()?,
108 usable_for_training: decoder.read_boolean()?,
109 });
110 }
111 decoder.finish()?;
112 Ok(StagedFragmentV1 {
113 analysis_txid,
114 transcript,
115 speakers,
116 })
117}
118
119pub fn encode_final_fragment(value: &FinalFragmentV1) -> Result<Vec<u8>, FormatError> {
120 let mut output = encode_fragment_prefix(
121 FINAL_KIND,
122 value.analysis_txid,
123 Some(value.confirmation_txid),
124 );
125 append_string(&mut output, &value.transcript)?;
126 append_length(&mut output, value.speakers.len())?;
127 for speaker in &value.speakers {
128 append_string(&mut output, &speaker.speaker.to_string())?;
129 append_string(&mut output, &speaker.person_id)?;
130 append_string(&mut output, &speaker.language)?;
131 append_features(&mut output, &speaker.features)?;
132 output.push(u8::from(speaker.usable_for_training));
133 }
134 Ok(output)
135}
136
137pub fn decode_final_fragment(bytes: &[u8]) -> Result<FinalFragmentV1, FormatError> {
138 let (analysis_txid, confirmation_txid) = decode_fragment_header(bytes, FINAL_KIND, true)?;
139 let mut decoder = BodyDecoder::new(&bytes[BODY_OFFSET..]);
140 let transcript = decoder.read_string()?;
141 let speaker_count = decoder.read_length()?;
142 let mut speakers = Vec::new();
143 for _ in 0..speaker_count {
144 speakers.push(FinalSpeakerV1 {
145 speaker: decoder.read_speaker_label()?,
146 person_id: decoder.read_string()?,
147 language: decoder.read_string()?,
148 features: decoder.read_features()?,
149 usable_for_training: decoder.read_boolean()?,
150 });
151 }
152 decoder.finish()?;
153 Ok(FinalFragmentV1 {
154 analysis_txid,
155 confirmation_txid: confirmation_txid
156 .expect("final fragment header always contains a confirmation slot"),
157 transcript,
158 speakers,
159 })
160}
161
162pub fn txid_path(txid: TxId) -> PathBuf {
163 let encoded =
164 String::from_utf8(encode_txid_base64(txid).to_vec()).expect("base64 alphabet is ASCII");
165 PathBuf::from(&encoded[..1]).join(format!("{}.dat", &encoded[1..]))
166}
167
168pub fn txid_from_path(path: impl AsRef<Path>) -> Result<TxId, FormatError> {
169 let mut components = path.as_ref().components();
170 let shard = normal_utf8_component(components.next())?;
171 let filename = normal_utf8_component(components.next())?;
172 if components.next().is_some() || shard.len() != 1 {
173 return Err(FormatError::InvalidPath);
174 }
175 let name = filename
176 .strip_suffix(".dat")
177 .ok_or(FormatError::InvalidPath)?;
178 if name.len() != 15 {
179 return Err(FormatError::InvalidPath);
180 }
181 let mut encoded = [0; 16];
182 encoded[0] = shard.as_bytes()[0];
183 encoded[1..].copy_from_slice(name.as_bytes());
184 decode_txid_base64(encoded)
185}
186
187fn encode_fragment_prefix(
188 kind: u8,
189 analysis_txid: TxId,
190 confirmation_txid: Option<TxId>,
191) -> Vec<u8> {
192 let mut output = vec![0; BODY_OFFSET];
193 output[0] = FRAGMENT_VERSION;
194 output[1] = kind;
195 output[ANALYSIS_SLOT_OFFSET..CONFIRMATION_SLOT_OFFSET]
196 .copy_from_slice(&TxIdSlot::new(analysis_txid).encode());
197 if let Some(txid) = confirmation_txid {
198 output[CONFIRMATION_SLOT_OFFSET..BODY_OFFSET]
199 .copy_from_slice(&TxIdSlot::new(txid).encode());
200 }
201 output
202}
203
204fn decode_fragment_header(
205 bytes: &[u8],
206 expected_kind: u8,
207 has_confirmation: bool,
208) -> Result<(TxId, Option<TxId>), FormatError> {
209 if bytes.len() < BODY_OFFSET {
210 return Err(FormatError::Truncated);
211 }
212 if bytes[0] != FRAGMENT_VERSION {
213 return Err(FormatError::UnsupportedVersion(bytes[0]));
214 }
215 if bytes[1] != expected_kind {
216 return Err(FormatError::InvalidFragmentKind(bytes[1]));
217 }
218 let reserved = &bytes[2..ANALYSIS_SLOT_OFFSET];
219 if reserved.iter().any(|byte| *byte != 0) {
220 return Err(FormatError::NonZeroReserved);
221 }
222 let analysis_txid =
223 TxIdSlot::decode(&bytes[ANALYSIS_SLOT_OFFSET..CONFIRMATION_SLOT_OFFSET])?.txid();
224 let confirmation_slot = &bytes[CONFIRMATION_SLOT_OFFSET..BODY_OFFSET];
225 let confirmation_txid = if has_confirmation {
226 Some(TxIdSlot::decode(confirmation_slot)?.txid())
227 } else {
228 if confirmation_slot.iter().any(|byte| *byte != 0) {
229 return Err(FormatError::NonZeroStagedConfirmation);
230 }
231 None
232 };
233 Ok((analysis_txid, confirmation_txid))
234}
235
236fn append_length(output: &mut Vec<u8>, length: usize) -> Result<(), FormatError> {
237 let length = u64::try_from(length).map_err(|_| FormatError::LengthOverflow)?;
238 output.extend_from_slice(&length.to_le_bytes());
239 Ok(())
240}
241
242fn append_bytes(output: &mut Vec<u8>, bytes: &[u8]) -> Result<(), FormatError> {
243 append_length(output, bytes.len())?;
244 output.extend_from_slice(bytes);
245 Ok(())
246}
247
248fn append_string(output: &mut Vec<u8>, value: &str) -> Result<(), FormatError> {
249 append_bytes(output, value.as_bytes())
250}
251
252fn append_features(output: &mut Vec<u8>, value: &FeatureVector24) -> Result<(), FormatError> {
253 let bytes = postcard::to_allocvec(value).map_err(|_| FormatError::InvalidFeatureBody)?;
254 append_bytes(output, &bytes)
255}
256
257struct BodyDecoder<'a> {
258 bytes: &'a [u8],
259 position: usize,
260}
261
262impl<'a> BodyDecoder<'a> {
263 fn new(bytes: &'a [u8]) -> Self {
264 Self { bytes, position: 0 }
265 }
266
267 fn take(&mut self, length: usize) -> Result<&'a [u8], FormatError> {
268 let end = self
269 .position
270 .checked_add(length)
271 .ok_or(FormatError::LengthOverflow)?;
272 let value = self
273 .bytes
274 .get(self.position..end)
275 .ok_or(FormatError::Truncated)?;
276 self.position = end;
277 Ok(value)
278 }
279
280 fn read_length(&mut self) -> Result<usize, FormatError> {
281 let mut bytes = [0; 8];
282 bytes.copy_from_slice(self.take(8)?);
283 usize::try_from(u64::from_le_bytes(bytes)).map_err(|_| FormatError::LengthOverflow)
284 }
285
286 fn read_bytes(&mut self) -> Result<&'a [u8], FormatError> {
287 let length = self.read_length()?;
288 self.take(length)
289 }
290
291 fn read_string(&mut self) -> Result<String, FormatError> {
292 Ok(std::str::from_utf8(self.read_bytes()?)
293 .map_err(|_| FormatError::InvalidUtf8)?
294 .to_owned())
295 }
296
297 fn read_speaker_label(&mut self) -> Result<LocalSpeakerLabel, FormatError> {
298 self.read_string()?
299 .parse()
300 .map_err(|_| FormatError::InvalidSpeakerLabel)
301 }
302
303 fn read_features(&mut self) -> Result<FeatureVector24, FormatError> {
304 let (value, remaining) = postcard::take_from_bytes(self.read_bytes()?)
305 .map_err(|_| FormatError::InvalidFeatureBody)?;
306 if !remaining.is_empty() {
307 return Err(FormatError::InvalidFeatureBody);
308 }
309 Ok(value)
310 }
311
312 fn read_boolean(&mut self) -> Result<bool, FormatError> {
313 match self.take(1)?[0] {
314 0 => Ok(false),
315 1 => Ok(true),
316 value => Err(FormatError::InvalidBoolean(value)),
317 }
318 }
319
320 fn finish(self) -> Result<(), FormatError> {
321 if self.position == self.bytes.len() {
322 Ok(())
323 } else {
324 Err(FormatError::TrailingBytes)
325 }
326 }
327}
328
329fn normal_utf8_component(component: Option<Component<'_>>) -> Result<&str, FormatError> {
330 match component {
331 Some(Component::Normal(value)) => value.to_str().ok_or(FormatError::InvalidPath),
332 _ => Err(FormatError::InvalidPath),
333 }
334}
335
336fn encode_txid_base64(txid: TxId) -> [u8; 16] {
337 let bytes = txid.into_bytes();
338 let mut encoded = [0; 16];
339 for group in 0..4 {
340 let input = group * 3;
341 let output = group * 4;
342 encoded[output] = BASE64_ALPHABET[(bytes[input] >> 2) as usize];
343 encoded[output + 1] =
344 BASE64_ALPHABET[(((bytes[input] & 3) << 4) | (bytes[input + 1] >> 4)) as usize];
345 encoded[output + 2] =
346 BASE64_ALPHABET[(((bytes[input + 1] & 15) << 2) | (bytes[input + 2] >> 6)) as usize];
347 encoded[output + 3] = BASE64_ALPHABET[(bytes[input + 2] & 63) as usize];
348 }
349 encoded
350}
351
352fn decode_txid_base64(encoded: [u8; 16]) -> Result<TxId, FormatError> {
353 let mut bytes = [0; 12];
354 for group in 0..4 {
355 let input = group * 4;
356 let output = group * 3;
357 let first = decode_base64_character(encoded[input])?;
358 let second = decode_base64_character(encoded[input + 1])?;
359 let third = decode_base64_character(encoded[input + 2])?;
360 let fourth = decode_base64_character(encoded[input + 3])?;
361 bytes[output] = (first << 2) | (second >> 4);
362 bytes[output + 1] = (second << 4) | (third >> 2);
363 bytes[output + 2] = (third << 6) | fourth;
364 }
365 Ok(TxId::from_bytes(bytes))
366}
367
368fn decode_base64_character(value: u8) -> Result<u8, FormatError> {
369 BASE64_ALPHABET
370 .iter()
371 .position(|candidate| *candidate == value)
372 .and_then(|index| u8::try_from(index).ok())
373 .ok_or(FormatError::InvalidPath)
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379
380 fn txid(seed: u8) -> TxId {
381 TxId::from_bytes([seed; 12])
382 }
383
384 fn staged_fragment() -> StagedFragmentV1 {
385 StagedFragmentV1 {
386 analysis_txid: txid(20),
387 transcript: "hello".into(),
388 speakers: vec![StagedSpeakerV1 {
389 speaker: LocalSpeakerLabel::new(1).expect("speaker label"),
390 language: "English".into(),
391 features: FeatureVector24::default(),
392 usable_for_training: true,
393 }],
394 }
395 }
396
397 fn final_fragment() -> FinalFragmentV1 {
398 FinalFragmentV1 {
399 analysis_txid: txid(30),
400 confirmation_txid: txid(40),
401 transcript: "hello".into(),
402 speakers: vec![FinalSpeakerV1 {
403 speaker: LocalSpeakerLabel::new(1).expect("speaker label"),
404 person_id: "person-1".into(),
405 language: "English".into(),
406 features: FeatureVector24::default(),
407 usable_for_training: false,
408 }],
409 }
410 }
411
412 #[test]
413 fn fragments_and_slots_roundtrip_with_exact_offsets() {
414 let staged = staged_fragment();
415 let staged_bytes = encode_staged_fragment(&staged).expect("encode staged");
416 assert_eq!(decode_staged_fragment(&staged_bytes), Ok(staged));
417
418 let final_value = final_fragment();
419 let final_bytes = encode_final_fragment(&final_value).expect("encode final");
420 assert_eq!(decode_final_fragment(&final_bytes), Ok(final_value));
421
422 assert_eq!(
423 TxIdSlot::decode(&[0; 15]),
424 Err(FormatError::InvalidTxIdSlotLength(15))
425 );
426 }
427
428 #[test]
429 fn malformed_headers_slots_and_bodies_are_rejected() {
430 let staged = staged_fragment();
431 let bytes = encode_staged_fragment(&staged).expect("encode staged");
432 let transcript = BODY_OFFSET + 8;
433 let label = transcript + staged.transcript.len() + 16;
434 let feature_length = label + staged.speakers[0].speaker.to_string().len() + 15;
435 let feature = feature_length + 8;
436 let boolean = feature
437 + postcard::to_allocvec(&staged.speakers[0].features)
438 .expect("features")
439 .len();
440 for (index, value, error) in [
441 (0, 2, FormatError::UnsupportedVersion(2)),
442 (1, 2, FormatError::InvalidFragmentKind(2)),
443 (2, 1, FormatError::NonZeroReserved),
444 (28, 1, FormatError::NonZeroPadding),
445 (32, 1, FormatError::NonZeroStagedConfirmation),
446 (transcript, 0xff, FormatError::InvalidUtf8),
447 (label, b'x', FormatError::InvalidSpeakerLabel),
448 (feature, 2, FormatError::InvalidFeatureBody),
449 (boolean, 2, FormatError::InvalidBoolean(2)),
450 ] {
451 let mut malformed = bytes.clone();
452 malformed[index] = value;
453 assert_eq!(decode_staged_fragment(&malformed), Err(error));
454 }
455 assert_eq!(
456 decode_staged_fragment(&bytes[..47]),
457 Err(FormatError::Truncated)
458 );
459 let mut trailing = bytes;
460 trailing.push(0);
461 assert_eq!(
462 decode_staged_fragment(&trailing),
463 Err(FormatError::TrailingBytes)
464 );
465 }
466
467 #[test]
468 fn every_path_shard_roundtrips_and_noncanonical_paths_fail() {
469 for (index, expected) in BASE64_ALPHABET.iter().enumerate() {
470 let mut bytes = [0; 12];
471 bytes[0] = (index as u8) << 2;
472 let txid = TxId::from_bytes(bytes);
473 let path = txid_path(txid);
474 assert_eq!(path.to_string_lossy().as_bytes()[0], *expected);
475 assert_eq!(txid_from_path(&path), Ok(txid));
476 }
477 for path in [
478 "/A/AAAAAAAAAAAAAAA.dat",
479 "AA/AAAAAAAAAAAAAAA.dat",
480 "A/AAAAAAAAAAAAAA!.dat",
481 "A/AAAAAAAAAAAAAAA.dat/extra",
482 ] {
483 assert_eq!(txid_from_path(path), Err(FormatError::InvalidPath));
484 }
485 }
486}