Skip to main content

record_descriptor/
lib.rs

1//! Public Bitneedle BRD1 descriptor wire-format and decoding primitives.
2//!
3//! This crate is the authoritative BRD1 carrier-descriptor wire contract. It
4//! describes how the record stream is located and encoded in the PNG carrier.
5//! It does not define BRS1 payload-entry semantics, programme-time revolution
6//! duration, track timing, GAP timing, or the relationship between payload
7//! entries and programme revolutions.
8//!
9//! It contains no JSON descriptor segments, no Brotli compatibility envelopes,
10//! no base64/hex wire representations, and no record-creation policy.
11
12use anyhow::{bail, Context, Result};
13use serde::{Deserialize, Serialize};
14
15pub const RECORD_DESCRIPTOR_MAGIC: &[u8; 4] = b"BRD1";
16pub const RECORD_DESCRIPTOR_VERSION: u8 = 2;
17pub const RECORD_DESCRIPTOR_PREFIX_LENGTH: usize = 19;
18
19pub const METADATA_GRAYSCALE_NIBBLE_BASE: u8 = 120;
20
21/// Fixed by the BRD1 v2 format: release commitments are always SHA-256,
22/// signatures are always Ed25519. No per-reference algorithm selector.
23pub const SIGNED_RELEASE_REFERENCE_VERSION: u8 = 2;
24pub const SIGNED_RELEASE_REFERENCE_HASH_LENGTH: usize = 32;
25pub const SIGNED_RELEASE_REFERENCE_SIGNATURE_LENGTH: usize = 64;
26pub const SIGNED_RELEASE_REFERENCE_MAX_KEY_ID_LENGTH: usize = u16::MAX as usize;
27
28pub const RECORD_PROFILE_SINGLE45_CODE: u8 = 0;
29pub const RECORD_PROFILE_LP_CODE: u8 = 1;
30pub const RECORD_PROFILE_SINGLE45: &str = "single45";
31pub const RECORD_PROFILE_LP: &str = "lp";
32
33pub const RELEASE_ID_LENGTH: usize = 16;
34
35pub const SEGMENT_DESCRIPTOR_CRC32: u8 = 1;
36pub const SEGMENT_STREAM_BYTE_LENGTH: u8 = 2;
37pub const SEGMENT_RECORD_PROFILE: u8 = 4;
38pub const SEGMENT_TITLE: u8 = 5;
39pub const SEGMENT_ARTIST: u8 = 6;
40pub const SEGMENT_PAYLOAD_ENCODING: u8 = 7;
41pub const SEGMENT_RELEASE_ID: u8 = 8;
42pub const SEGMENT_CATALOG_NUMBER: u8 = 9;
43pub const SEGMENT_LABEL: u8 = 10;
44pub const SEGMENT_ARTWORK_CREDIT: u8 = 11;
45pub const SEGMENT_CANONICAL_URL: u8 = 13;
46pub const SEGMENT_CREATED_AT: u8 = 14;
47pub const SEGMENT_SIGNED_RELEASE_REFERENCE: u8 = 16;
48pub const SEGMENT_BSC_POINTER: u8 = 21;
49pub const SEGMENT_TONED_CARRIER_MAP: u8 = 22;
50
51pub const PAYLOAD_ENCODING_RGB: &str = "rgb";
52pub const PAYLOAD_ENCODING_TONED_V1: &str = "toned-v1";
53pub const PAYLOAD_ENCODING_RGB_CODE: u8 = 0;
54pub const PAYLOAD_ENCODING_TONED_V1_CODE: u8 = 1;
55
56pub const TONED_CARRIER_MAP_VERSION: u8 = 1;
57pub const TONED_ORDERING_BASE_PROXIMITY: u8 = 0;
58pub const TONED_ORDERING_CHROMA_PROXIMITY: u8 = 1;
59pub const TONED_MIN_BITS_PER_PIXEL: u8 = 1;
60pub const TONED_MAX_BITS_PER_PIXEL: u8 = 24;
61pub const TONED_MAX_SPAN_COUNT: usize = u16::MAX as usize;
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
64#[serde(rename_all = "camelCase")]
65pub enum ToneOrdering {
66    BaseProximity,
67    ChromaProximity,
68}
69
70impl ToneOrdering {
71    pub fn wire_code(self) -> u8 {
72        match self {
73            Self::BaseProximity => TONED_ORDERING_BASE_PROXIMITY,
74            Self::ChromaProximity => TONED_ORDERING_CHROMA_PROXIMITY,
75        }
76    }
77
78    pub fn from_wire_code(code: u8) -> Result<Self> {
79        match code {
80            TONED_ORDERING_BASE_PROXIMITY => Ok(Self::BaseProximity),
81            TONED_ORDERING_CHROMA_PROXIMITY => Ok(Self::ChromaProximity),
82            _ => bail!("unknown toned carrier ordering code {code}"),
83        }
84    }
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
88#[serde(rename_all = "camelCase")]
89pub struct ToneSpanDescriptor {
90    pub byte_length: usize,
91    pub base: [u8; 3],
92    pub luma_tolerance: u8,
93    pub bits_per_pixel: u8,
94    pub ordering: ToneOrdering,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
98#[serde(rename_all = "camelCase")]
99pub struct ResolvedToneSpan {
100    pub index: usize,
101    pub byte_offset: usize,
102    pub byte_length: usize,
103    pub pixel_offset: usize,
104    pub pixel_count: usize,
105    pub base: [u8; 3],
106    pub luma_tolerance: u8,
107    pub bits_per_pixel: u8,
108    pub ordering: ToneOrdering,
109}
110
111/// One blanket signed-release reference covering the release commitment
112/// (see `record_core::commitment::release_commitment`). SHA-256 and Ed25519
113/// are fixed by `SIGNED_RELEASE_REFERENCE_VERSION`; there is no per-reference
114/// algorithm selector.
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
116#[serde(rename_all = "camelCase")]
117pub struct SignedReleaseReference {
118    pub version: u8,
119    pub release_commitment_sha256: [u8; SIGNED_RELEASE_REFERENCE_HASH_LENGTH],
120    pub key_id: Vec<u8>,
121    pub signature: Vec<u8>,
122}
123
124impl SignedReleaseReference {
125    pub fn validate(&self) -> Result<()> {
126        if self.version != SIGNED_RELEASE_REFERENCE_VERSION {
127            bail!(
128                "unsupported signed release reference version: {}",
129                self.version
130            );
131        }
132        if self.key_id.is_empty() {
133            bail!("signature key ID must not be empty");
134        }
135        if self.key_id.len() > SIGNED_RELEASE_REFERENCE_MAX_KEY_ID_LENGTH {
136            bail!("signature key ID exceeds u16 length limit");
137        }
138        if self.signature.len() != SIGNED_RELEASE_REFERENCE_SIGNATURE_LENGTH {
139            bail!("signature must be exactly {SIGNED_RELEASE_REFERENCE_SIGNATURE_LENGTH} bytes");
140        }
141        Ok(())
142    }
143}
144
145/// Decoded BRD1 carrier descriptor.
146///
147/// `record_profile` identifies the canonical Bitneedle carrier profile used to
148/// decode the raster geometry. BRD1 does not use this field to assign logical
149/// sample counts to BRS1 payload entries; programme timing belongs to BRS1 and
150/// codec-specific validation.
151#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
152#[serde(rename_all = "camelCase")]
153pub struct RecordDescriptor {
154    pub version: u8,
155    pub checksum_protected: bool,
156    pub b_value_bits: u64,
157    pub record_profile: String,
158    pub stream_byte_length: usize,
159    pub payload_encoding: String,
160    pub title: Option<String>,
161    pub artist: Option<String>,
162    pub release_id: Option<[u8; RELEASE_ID_LENGTH]>,
163    pub catalog_number: Option<String>,
164    pub label: Option<String>,
165    pub artwork_credit: Option<String>,
166    pub canonical_url: Option<String>,
167    pub created_at: Option<u64>,
168    pub signed_release_reference: Option<SignedReleaseReference>,
169    pub bsc_pointer: Option<Vec<u8>>,
170    pub tone_spans: Vec<ToneSpanDescriptor>,
171}
172
173impl RecordDescriptor {
174    pub fn b_value(&self) -> f64 {
175        f64::from_bits(self.b_value_bits)
176    }
177}
178
179#[derive(Debug, Clone, PartialEq, Eq)]
180pub struct DescriptorPrefix {
181    pub version: u8,
182    pub payload_len: usize,
183    pub segment_count: usize,
184    pub segment_stream_len: usize,
185    pub b_value_bits: u64,
186}
187
188pub fn metadata_pixel_count_for_byte_length(byte_length: usize) -> usize {
189    byte_length.saturating_mul(2)
190}
191
192pub fn metadata_byte_capacity_for_pixel_count(pixel_count: usize) -> usize {
193    pixel_count / 2
194}
195
196pub fn metadata_bytes_from_grayscale_rgba(
197    rgba: &[u8],
198    indices: &[usize],
199    byte_length: usize,
200    label: &str,
201) -> Result<Vec<u8>> {
202    let pixel_count = metadata_pixel_count_for_byte_length(byte_length);
203    if indices.len() < pixel_count {
204        bail!("{label} spiral capacity is too small");
205    }
206
207    let mut bytes = Vec::with_capacity(byte_length);
208    for byte_number in 0..byte_length {
209        let mut nibbles = [0u8; 2];
210        for nibble_index in 0..2 {
211            let pixel_index = indices[byte_number * 2 + nibble_index];
212            let rgba_index = pixel_index
213                .checked_mul(4)
214                .context("metadata RGBA index overflow")?;
215            if rgba_index + 3 >= rgba.len() {
216                bail!("{label} spiral pixel index is outside RGBA buffer");
217            }
218
219            let red = rgba[rgba_index];
220            let green = rgba[rgba_index + 1];
221            let blue = rgba[rgba_index + 2];
222            let alpha = rgba[rgba_index + 3];
223
224            if alpha == 0 {
225                bail!("{label} spiral pixel is empty");
226            }
227            if red != green || green != blue {
228                bail!("{label} metadata pixel is not grayscale");
229            }
230
231            let nibble = red
232                .checked_sub(METADATA_GRAYSCALE_NIBBLE_BASE)
233                .context("metadata pixel is below grayscale nibble range")?;
234            if nibble > 0x0f {
235                bail!("{label} metadata pixel is outside grayscale nibble range");
236            }
237            nibbles[nibble_index] = nibble;
238        }
239        bytes.push((nibbles[0] << 4) | nibbles[1]);
240    }
241    Ok(bytes)
242}
243
244pub fn record_profile_code(record_profile: &str) -> Result<u8> {
245    match record_profile {
246        RECORD_PROFILE_SINGLE45 => Ok(RECORD_PROFILE_SINGLE45_CODE),
247        RECORD_PROFILE_LP => Ok(RECORD_PROFILE_LP_CODE),
248        other => bail!("unsupported canonical record profile {other}"),
249    }
250}
251
252pub fn record_profile_from_code(code: u8) -> Result<String> {
253    match code {
254        RECORD_PROFILE_SINGLE45_CODE => Ok(RECORD_PROFILE_SINGLE45.to_string()),
255        RECORD_PROFILE_LP_CODE => Ok(RECORD_PROFILE_LP.to_string()),
256        other => bail!("unknown record profile code {other}"),
257    }
258}
259
260pub fn payload_encoding_code(payload_encoding: &str) -> Result<u8> {
261    match payload_encoding {
262        PAYLOAD_ENCODING_RGB => Ok(PAYLOAD_ENCODING_RGB_CODE),
263        PAYLOAD_ENCODING_TONED_V1 => Ok(PAYLOAD_ENCODING_TONED_V1_CODE),
264        other => bail!("unsupported canonical payload encoding {other}"),
265    }
266}
267
268pub fn payload_encoding_from_code(code: u8) -> Result<String> {
269    match code {
270        PAYLOAD_ENCODING_RGB_CODE => Ok(PAYLOAD_ENCODING_RGB.to_string()),
271        PAYLOAD_ENCODING_TONED_V1_CODE => Ok(PAYLOAD_ENCODING_TONED_V1.to_string()),
272        other => bail!("unknown payload encoding code {other}"),
273    }
274}
275
276const RELEASE_ID_TAGGED_PREFIX: &str = "rel_";
277const RELEASE_ID_ULID_TEXT_LENGTH: usize = 26;
278const CROCKFORD_BASE32: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
279
280/// Parse a canonical `rel_`-tagged ULID string into its 16 raw bytes.
281pub fn release_id_to_bytes(text: &str) -> Result<[u8; RELEASE_ID_LENGTH]> {
282    let ulid_text = text
283        .strip_prefix(RELEASE_ID_TAGGED_PREFIX)
284        .context("release ID is missing the rel_ prefix")?;
285    if ulid_text.len() != RELEASE_ID_ULID_TEXT_LENGTH {
286        bail!("release ID must be 26 Crockford Base32 characters");
287    }
288
289    let mut bits: u128 = 0;
290    for (index, byte) in ulid_text.bytes().enumerate() {
291        let upper = byte.to_ascii_uppercase();
292        let digit = CROCKFORD_BASE32
293            .iter()
294            .position(|&candidate| candidate == upper)
295            .context("release ID contains a non-canonical Crockford Base32 character")?;
296
297        // A textual ULID contains 130 encoded bits but the value is only
298        // 128 bits. Therefore the first Crockford digit may contain only the
299        // low two bits (0..=7). Rejecting larger values prevents silent u128
300        // truncation and ensures one canonical text representation per value.
301        if index == 0 && digit > 7 {
302            bail!("release ID exceeds the 128-bit ULID range");
303        }
304
305        bits = (bits << 5) | digit as u128;
306    }
307    Ok(bits.to_be_bytes())
308}
309
310/// Format 16 raw release ULID bytes back into the canonical `rel_`-tagged
311/// display string.
312pub fn release_id_to_text(bytes: [u8; RELEASE_ID_LENGTH]) -> String {
313    let mut value = u128::from_be_bytes(bytes);
314    let mut chars = [b'0'; RELEASE_ID_ULID_TEXT_LENGTH];
315    for index in (0..RELEASE_ID_ULID_TEXT_LENGTH).rev() {
316        chars[index] = CROCKFORD_BASE32[(value & 0x1f) as usize];
317        value >>= 5;
318    }
319    let mut text = String::with_capacity(RELEASE_ID_TAGGED_PREFIX.len() + RELEASE_ID_ULID_TEXT_LENGTH);
320    text.push_str(RELEASE_ID_TAGGED_PREFIX);
321    text.push_str(std::str::from_utf8(&chars).expect("Crockford Base32 alphabet is ASCII"));
322    text
323}
324
325pub fn decode_descriptor_prefix(bytes: &[u8]) -> Result<DescriptorPrefix> {
326    if bytes.len() < RECORD_DESCRIPTOR_PREFIX_LENGTH {
327        bail!("record descriptor payload too short");
328    }
329    if &bytes[..4] != RECORD_DESCRIPTOR_MAGIC {
330        bail!("record descriptor magic mismatch");
331    }
332
333    let version = bytes[4];
334    let payload_len =
335        u16::from_be_bytes(bytes[5..7].try_into().expect("slice length")) as usize;
336    let segment_count =
337        u16::from_be_bytes(bytes[7..9].try_into().expect("slice length")) as usize;
338    let segment_stream_len =
339        u16::from_be_bytes(bytes[9..11].try_into().expect("slice length")) as usize;
340    let b_value_bits =
341        u64::from_be_bytes(bytes[11..19].try_into().expect("slice length"));
342
343    if payload_len < RECORD_DESCRIPTOR_PREFIX_LENGTH || payload_len > bytes.len() {
344        bail!("record descriptor payload length is invalid");
345    }
346
347    Ok(DescriptorPrefix {
348        version,
349        payload_len,
350        segment_count,
351        segment_stream_len,
352        b_value_bits,
353    })
354}
355
356pub fn validate_tone_span(span: &ToneSpanDescriptor, index: usize) -> Result<()> {
357    if span.byte_length == 0 {
358        bail!("tone span {index} byte length must be greater than zero");
359    }
360    if !(TONED_MIN_BITS_PER_PIXEL..=TONED_MAX_BITS_PER_PIXEL)
361        .contains(&span.bits_per_pixel)
362    {
363        bail!(
364            "tone span {index} bits per pixel must be between {} and {}",
365            TONED_MIN_BITS_PER_PIXEL,
366            TONED_MAX_BITS_PER_PIXEL
367        );
368    }
369    Ok(())
370}
371
372pub fn resolve_tone_spans(
373    spans: &[ToneSpanDescriptor],
374    expected_byte_length: Option<usize>,
375) -> Result<Vec<ResolvedToneSpan>> {
376    if spans.is_empty() {
377        bail!("toned-v1 carrier map must contain at least one span");
378    }
379    if spans.len() > TONED_MAX_SPAN_COUNT {
380        bail!("tone span count exceeds u16 range");
381    }
382
383    let mut byte_offset = 0usize;
384    let mut pixel_offset = 0usize;
385    let mut resolved = Vec::with_capacity(spans.len());
386
387    for (index, span) in spans.iter().enumerate() {
388        validate_tone_span(span, index)?;
389        let bit_length = span
390            .byte_length
391            .checked_mul(8)
392            .context("tone span bit length overflow")?;
393        let pixel_count =
394            bit_length.div_ceil(usize::from(span.bits_per_pixel));
395
396        resolved.push(ResolvedToneSpan {
397            index,
398            byte_offset,
399            byte_length: span.byte_length,
400            pixel_offset,
401            pixel_count,
402            base: span.base,
403            luma_tolerance: span.luma_tolerance,
404            bits_per_pixel: span.bits_per_pixel,
405            ordering: span.ordering,
406        });
407
408        byte_offset = byte_offset
409            .checked_add(span.byte_length)
410            .context("tone span total byte length overflow")?;
411        pixel_offset = pixel_offset
412            .checked_add(pixel_count)
413            .context("tone span total pixel count overflow")?;
414    }
415
416    if let Some(expected) = expected_byte_length {
417        if byte_offset != expected {
418            bail!(
419                "tone spans cover {byte_offset} bytes, expected {expected}"
420            );
421        }
422    }
423
424    Ok(resolved)
425}
426
427pub fn toned_pixel_count(
428    spans: &[ToneSpanDescriptor],
429    expected_byte_length: Option<usize>,
430) -> Result<usize> {
431    Ok(resolve_tone_spans(spans, expected_byte_length)?
432        .last()
433        .map(|span| span.pixel_offset + span.pixel_count)
434        .unwrap_or(0))
435}
436
437pub fn encode_toned_carrier_map(
438    spans: &[ToneSpanDescriptor],
439    expected_byte_length: Option<usize>,
440) -> Result<Vec<u8>> {
441    resolve_tone_spans(spans, expected_byte_length)?;
442
443    let mut out = Vec::new();
444    out.push(TONED_CARRIER_MAP_VERSION);
445    out.extend_from_slice(
446        &u16::try_from(spans.len())
447            .context("tone span count exceeds u16")?
448            .to_be_bytes(),
449    );
450
451    for span in spans {
452        push_varuint(
453            &mut out,
454            u64::try_from(span.byte_length)
455                .context("tone span byte length exceeds u64")?,
456        );
457        out.extend_from_slice(&span.base);
458        out.push(span.luma_tolerance);
459        out.push(span.bits_per_pixel);
460        out.push(span.ordering.wire_code());
461    }
462
463    Ok(out)
464}
465
466pub fn decode_toned_carrier_map(
467    bytes: &[u8],
468    expected_byte_length: Option<usize>,
469) -> Result<Vec<ToneSpanDescriptor>> {
470    let mut cursor = ByteCursor::new(bytes);
471    let version = cursor.read_u8("toned carrier map version")?;
472    if version != TONED_CARRIER_MAP_VERSION {
473        bail!("unsupported toned carrier map version {version}");
474    }
475
476    let count = usize::from(cursor.read_u16be("tone span count")?);
477    if count == 0 {
478        bail!("toned-v1 carrier map must contain at least one span");
479    }
480
481    let mut spans = Vec::with_capacity(count);
482    for index in 0..count {
483        let byte_length = usize::try_from(
484            cursor.read_varuint("tone span byte length")?,
485        )
486        .context("tone span byte length exceeds usize")?;
487        let base = [
488            cursor.read_u8("tone span base red")?,
489            cursor.read_u8("tone span base green")?,
490            cursor.read_u8("tone span base blue")?,
491        ];
492        let luma_tolerance =
493            cursor.read_u8("tone span luma tolerance")?;
494        let bits_per_pixel =
495            cursor.read_u8("tone span bits per pixel")?;
496        let ordering = ToneOrdering::from_wire_code(
497            cursor.read_u8("tone span ordering")?,
498        )?;
499
500        let span = ToneSpanDescriptor {
501            byte_length,
502            base,
503            luma_tolerance,
504            bits_per_pixel,
505            ordering,
506        };
507        validate_tone_span(&span, index)?;
508        spans.push(span);
509    }
510
511    if cursor.remaining() != 0 {
512        bail!(
513            "toned carrier map contains {} trailing bytes",
514            cursor.remaining()
515        );
516    }
517
518    resolve_tone_spans(&spans, expected_byte_length)?;
519    Ok(spans)
520}
521
522fn push_varuint(out: &mut Vec<u8>, mut value: u64) {
523    loop {
524        let mut byte = (value & 0x7f) as u8;
525        value >>= 7;
526        if value != 0 {
527            byte |= 0x80;
528        }
529        out.push(byte);
530        if value == 0 {
531            break;
532        }
533    }
534}
535
536pub fn decode_signed_release_reference(bytes: &[u8]) -> Result<SignedReleaseReference> {
537    let mut cursor = ByteCursor::new(bytes);
538
539    let version = cursor.read_u8("signed release reference version")?;
540    let release_commitment_sha256 = cursor
541        .read_bytes(
542            SIGNED_RELEASE_REFERENCE_HASH_LENGTH,
543            "release commitment SHA-256",
544        )?
545        .try_into()
546        .expect("length checked");
547    let key_id_len = cursor.read_u16be("signature key ID length")? as usize;
548    let key_id = cursor.read_bytes(key_id_len, "signature key ID")?.to_vec();
549    let signature = cursor
550        .read_bytes(SIGNED_RELEASE_REFERENCE_SIGNATURE_LENGTH, "signature")?
551        .to_vec();
552
553    if cursor.remaining() != 0 {
554        bail!(
555            "signed release reference contains {} trailing bytes",
556            cursor.remaining()
557        );
558    }
559
560    let reference = SignedReleaseReference {
561        version,
562        release_commitment_sha256,
563        key_id,
564        signature,
565    };
566    reference.validate()?;
567    Ok(reference)
568}
569
570pub fn decode_record_descriptor_bytes(bytes: &[u8]) -> Result<RecordDescriptor> {
571    let prefix = decode_descriptor_prefix(bytes)?;
572
573    if prefix.version != RECORD_DESCRIPTOR_VERSION {
574        bail!("record descriptor version mismatch");
575    }
576    if prefix.payload_len != RECORD_DESCRIPTOR_PREFIX_LENGTH + prefix.segment_stream_len {
577        bail!("record descriptor segment stream length mismatch");
578    }
579
580    let body = &bytes[RECORD_DESCRIPTOR_PREFIX_LENGTH..prefix.payload_len];
581    let mut offset = 0usize;
582    let mut parsed_segments = 0usize;
583
584    let mut crc32_range = None;
585    let mut crc32 = None;
586    let mut stream_byte_length = None;
587    let mut record_profile = None;
588    let mut payload_encoding = None;
589    let mut title = None;
590    let mut artist = None;
591    let mut release_id = None;
592    let mut catalog_number = None;
593    let mut label = None;
594    let mut artwork_credit = None;
595    let mut canonical_url = None;
596    let mut created_at = None;
597    let mut signed_release_reference = None;
598    let mut bsc_pointer = None;
599    let mut tone_spans = None;
600
601    while offset < body.len() {
602        if parsed_segments >= prefix.segment_count {
603            bail!("record descriptor contains more segments than declared");
604        }
605        if offset + 3 > body.len() {
606            bail!("record descriptor segment is truncated");
607        }
608
609        let kind = body[offset];
610        let len = u16::from_be_bytes(
611            body[offset + 1..offset + 3]
612                .try_into()
613                .expect("slice length"),
614        ) as usize;
615        let payload_start = offset + 3;
616        let payload_end = payload_start
617            .checked_add(len)
618            .context("record descriptor segment length overflow")?;
619        if payload_end > body.len() {
620            bail!("record descriptor segment payload is truncated");
621        }
622
623        let payload = &body[payload_start..payload_end];
624        match kind {
625            SEGMENT_DESCRIPTOR_CRC32 => {
626                if crc32.is_some() {
627                    bail!("duplicate record descriptor CRC32 segment");
628                }
629                if payload.len() != 4 {
630                    bail!("record descriptor CRC32 segment has invalid length");
631                }
632                crc32 = Some(u32::from_be_bytes(
633                    payload.try_into().expect("slice length"),
634                ));
635                let absolute_start = RECORD_DESCRIPTOR_PREFIX_LENGTH + payload_start;
636                crc32_range = Some(absolute_start..absolute_start + payload.len());
637            }
638            SEGMENT_STREAM_BYTE_LENGTH => {
639                if stream_byte_length.is_some() {
640                    bail!("duplicate stream byte length segment");
641                }
642                if payload.len() != 4 {
643                    bail!("stream byte length segment has invalid length");
644                }
645                let raw_len = u32::from_be_bytes(payload.try_into().expect("slice length"));
646                if raw_len == 0 {
647                    bail!("stream byte length must not be zero");
648                }
649                stream_byte_length = Some(raw_len as usize);
650            }
651            SEGMENT_RECORD_PROFILE => {
652                if payload.len() != 1 {
653                    bail!("record profile segment has invalid length");
654                }
655                assign_once(
656                    &mut record_profile,
657                    record_profile_from_code(payload[0])?,
658                    "record profile",
659                )?
660            }
661            SEGMENT_PAYLOAD_ENCODING => {
662                if payload.len() != 1 {
663                    bail!("payload encoding segment has invalid length");
664                }
665                assign_once(
666                    &mut payload_encoding,
667                    payload_encoding_from_code(payload[0])?,
668                    "payload encoding",
669                )?
670            }
671            SEGMENT_TITLE => assign_once(
672                &mut title,
673                decode_optional_text(payload, "title")?,
674                "title",
675            )?,
676            SEGMENT_ARTIST => assign_once(
677                &mut artist,
678                decode_optional_text(payload, "artist")?,
679                "artist",
680            )?,
681            SEGMENT_RELEASE_ID => {
682                if payload.len() != RELEASE_ID_LENGTH {
683                    bail!("release ID segment has invalid length");
684                }
685                assign_once(
686                    &mut release_id,
687                    <[u8; RELEASE_ID_LENGTH]>::try_from(payload).expect("length checked"),
688                    "release ID",
689                )?
690            }
691            SEGMENT_CATALOG_NUMBER => assign_once(
692                &mut catalog_number,
693                decode_optional_text(payload, "catalog number")?,
694                "catalog number",
695            )?,
696            SEGMENT_LABEL => assign_once(
697                &mut label,
698                decode_optional_text(payload, "label")?,
699                "label",
700            )?,
701            SEGMENT_ARTWORK_CREDIT => assign_once(
702                &mut artwork_credit,
703                decode_optional_text(payload, "artwork credit")?,
704                "artwork credit",
705            )?,
706            SEGMENT_CANONICAL_URL => assign_once(
707                &mut canonical_url,
708                decode_optional_text(payload, "canonical URL")?,
709                "canonical URL",
710            )?,
711            SEGMENT_CREATED_AT => {
712                if payload.len() != 8 {
713                    bail!("created-at segment has invalid length");
714                }
715                assign_once(
716                    &mut created_at,
717                    u64::from_be_bytes(payload.try_into().expect("slice length")),
718                    "created-at timestamp",
719                )?
720            }
721            SEGMENT_SIGNED_RELEASE_REFERENCE => {
722                if signed_release_reference.is_some() {
723                    bail!("duplicate signed release reference segment");
724                }
725                signed_release_reference =
726                    Some(decode_signed_release_reference(payload)?);
727            }
728            SEGMENT_BSC_POINTER => {
729                if bsc_pointer.is_some() {
730                    bail!("duplicate BSC pointer segment");
731                }
732                if payload.is_empty() {
733                    bail!("BSC pointer segment must not be empty");
734                }
735                bsc_pointer = Some(payload.to_vec());
736            }
737            SEGMENT_TONED_CARRIER_MAP => {
738                if tone_spans.is_some() {
739                    bail!("duplicate toned carrier map segment");
740                }
741                tone_spans = Some(decode_toned_carrier_map(payload, None)?);
742            }
743            _ => bail!("unsupported canonical record descriptor segment type {kind}"),
744        }
745
746        offset = payload_end;
747        parsed_segments += 1;
748    }
749
750    if parsed_segments != prefix.segment_count {
751        bail!(
752            "record descriptor segment count mismatch: declared {}, parsed {}",
753            prefix.segment_count,
754            parsed_segments
755        );
756    }
757
758    let expected = crc32.context("record descriptor CRC32 segment is missing")?;
759    let range = crc32_range.context("record descriptor CRC32 segment is missing")?;
760    let mut canonical = bytes[..prefix.payload_len].to_vec();
761    canonical[range].fill(0);
762
763    if compute_descriptor_crc32(&canonical) != expected {
764        bail!("record descriptor CRC32 mismatch");
765    }
766
767    let b_value = f64::from_bits(prefix.b_value_bits);
768    if !(b_value.is_finite() && b_value > 0.0) {
769        bail!("decoded invalid b_value");
770    }
771
772    let record_profile =
773        record_profile.context("record profile segment is missing")?;
774    let stream_byte_length = stream_byte_length
775        .context("stream byte length segment is missing")?;
776    let payload_encoding =
777        payload_encoding.context("payload encoding segment is missing")?;
778    let tone_spans = tone_spans.unwrap_or_default();
779
780    match payload_encoding.as_str() {
781        PAYLOAD_ENCODING_RGB => {
782            if !tone_spans.is_empty() {
783                bail!("rgb payload encoding must not include a toned carrier map");
784            }
785        }
786        PAYLOAD_ENCODING_TONED_V1 => {
787            if tone_spans.is_empty() {
788                bail!("toned-v1 payload encoding requires a toned carrier map");
789            }
790            resolve_tone_spans(&tone_spans, Some(stream_byte_length))?;
791        }
792        other => bail!("unsupported canonical payload encoding {other}"),
793    }
794
795    Ok(RecordDescriptor {
796        version: prefix.version,
797        checksum_protected: true,
798        b_value_bits: prefix.b_value_bits,
799        record_profile,
800        stream_byte_length,
801        payload_encoding,
802        title: title.flatten(),
803        artist: artist.flatten(),
804        release_id,
805        catalog_number: catalog_number.flatten(),
806        label: label.flatten(),
807        artwork_credit: artwork_credit.flatten(),
808        canonical_url: canonical_url.flatten(),
809        created_at,
810        signed_release_reference,
811        bsc_pointer,
812        tone_spans,
813    })
814}
815
816pub fn compute_descriptor_crc32(bytes: &[u8]) -> u32 {
817    record_core::crc32_ieee(bytes)
818}
819
820fn decode_optional_text(payload: &[u8], label: &str) -> Result<Option<String>> {
821    if payload.is_empty() {
822        return Ok(None);
823    }
824    Ok(Some(decode_text(payload, label)?))
825}
826
827fn decode_text(payload: &[u8], label: &str) -> Result<String> {
828    let value = String::from_utf8(payload.to_vec())
829        .with_context(|| format!("record descriptor {label} is not valid UTF-8"))?;
830    if value.chars().any(char::is_control) {
831        bail!("record descriptor {label} contains control characters");
832    }
833    Ok(value)
834}
835
836fn assign_once<T>(
837    destination: &mut Option<T>,
838    value: T,
839    label: &str,
840) -> Result<()> {
841    if destination.is_some() {
842        bail!("duplicate {label} segment");
843    }
844    *destination = Some(value);
845    Ok(())
846}
847
848#[derive(Clone, Copy)]
849struct ByteCursor<'a> {
850    bytes: &'a [u8],
851    offset: usize,
852}
853
854impl<'a> ByteCursor<'a> {
855    fn new(bytes: &'a [u8]) -> Self {
856        Self { bytes, offset: 0 }
857    }
858
859    fn remaining(self) -> usize {
860        self.bytes.len().saturating_sub(self.offset)
861    }
862
863    fn read_u8(&mut self, label: &str) -> Result<u8> {
864        let value = *self
865            .bytes
866            .get(self.offset)
867            .with_context(|| format!("{label} is truncated"))?;
868        self.offset += 1;
869        Ok(value)
870    }
871
872    fn read_u16be(&mut self, label: &str) -> Result<u16> {
873        let end = self
874            .offset
875            .checked_add(2)
876            .with_context(|| format!("{label} offset overflow"))?;
877        let bytes = self
878            .bytes
879            .get(self.offset..end)
880            .with_context(|| format!("{label} is truncated"))?;
881        self.offset = end;
882        Ok(u16::from_be_bytes(bytes.try_into().expect("length checked")))
883    }
884
885    fn read_varuint(&mut self, label: &str) -> Result<u64> {
886        let start = self.offset;
887        let mut value = 0u64;
888        let mut shift = 0u32;
889
890        for byte_index in 0..10 {
891            let byte = self.read_u8(label)?;
892            let payload = u64::from(byte & 0x7f);
893
894            if shift == 63 && payload > 1 {
895                bail!("{label} exceeds u64 range");
896            }
897
898            value |= payload
899                .checked_shl(shift)
900                .with_context(|| format!("{label} shift overflow"))?;
901
902            if byte & 0x80 == 0 {
903                let consumed = self.offset - start;
904                if consumed > 1 {
905                    let minimum = 1u64 << (7 * (consumed - 1));
906                    if value < minimum {
907                        bail!(
908                            "{label} uses non-canonical overlong varuint encoding"
909                        );
910                    }
911                }
912                return Ok(value);
913            }
914
915            shift += 7;
916            if byte_index == 9 {
917                bail!("{label} exceeds ten-byte varuint limit");
918            }
919        }
920
921        unreachable!()
922    }
923
924    fn read_bytes(&mut self, length: usize, label: &str) -> Result<&'a [u8]> {
925        let end = self
926            .offset
927            .checked_add(length)
928            .with_context(|| format!("{label} length overflow"))?;
929        let bytes = self
930            .bytes
931            .get(self.offset..end)
932            .with_context(|| format!("{label} is truncated"))?;
933        self.offset = end;
934        Ok(bytes)
935    }
936}
937
938#[cfg(test)]
939mod tests {
940    use super::*;
941
942    #[test]
943    fn record_profile_codes_round_trip() {
944        assert_eq!(record_profile_code("single45").unwrap(), 0);
945        assert_eq!(record_profile_code("lp").unwrap(), 1);
946        assert_eq!(record_profile_from_code(0).unwrap(), "single45");
947        assert_eq!(record_profile_from_code(1).unwrap(), "lp");
948        assert!(record_profile_from_code(2).is_err());
949    }
950
951    #[test]
952    fn payload_encoding_codes_round_trip() {
953        assert_eq!(payload_encoding_code("rgb").unwrap(), 0);
954        assert_eq!(payload_encoding_code("toned-v1").unwrap(), 1);
955        assert_eq!(payload_encoding_from_code(0).unwrap(), "rgb");
956        assert_eq!(payload_encoding_from_code(1).unwrap(), "toned-v1");
957        assert!(payload_encoding_from_code(2).is_err());
958    }
959
960    #[test]
961    fn release_id_text_round_trips_through_bytes() {
962        let bytes = [
963            0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60,
964            0x70, 0x80,
965        ];
966        let text = release_id_to_text(bytes);
967        assert!(text.starts_with("rel_"));
968        assert_eq!(text.len(), 4 + 26);
969        assert_eq!(release_id_to_bytes(&text).unwrap(), bytes);
970    }
971
972    #[test]
973    fn release_id_rejects_missing_prefix() {
974        assert!(release_id_to_bytes("01ARZ3NDEKTSV4RRFFQ69G5FAV").is_err());
975    }
976
977    #[test]
978    fn release_id_rejects_values_above_the_ulid_range() {
979        assert!(
980            release_id_to_bytes("rel_Z1ARZ3NDEKTSV4RRFFQ69G5FAV").is_err()
981        );
982    }
983
984    #[test]
985    fn release_id_accepts_the_maximum_canonical_ulid() {
986        let text = "rel_7ZZZZZZZZZZZZZZZZZZZZZZZZZ";
987        let bytes = release_id_to_bytes(text).unwrap();
988        assert_eq!(release_id_to_text(bytes), text);
989    }
990
991    #[test]
992    fn binary_reference_round_trips_through_decoder() {
993        let mut bytes = Vec::new();
994        bytes.push(SIGNED_RELEASE_REFERENCE_VERSION);
995        bytes.extend_from_slice(&[0x11; 32]);
996        bytes.extend_from_slice(&3u16.to_be_bytes());
997        bytes.extend_from_slice(b"key");
998        bytes.extend_from_slice(&[0x22; 64]);
999
1000        let decoded = decode_signed_release_reference(&bytes).unwrap();
1001        assert_eq!(decoded.release_commitment_sha256, [0x11; 32]);
1002        assert_eq!(decoded.key_id, b"key");
1003        assert_eq!(decoded.signature, vec![0x22; 64]);
1004    }
1005    #[test]
1006    fn toned_carrier_map_round_trips() {
1007        let spans = vec![
1008            ToneSpanDescriptor {
1009                byte_length: 1024,
1010                base: [255, 192, 203],
1011                luma_tolerance: 16,
1012                bits_per_pixel: 21,
1013                ordering: ToneOrdering::ChromaProximity,
1014            },
1015            ToneSpanDescriptor {
1016                byte_length: 513,
1017                base: [20, 40, 80],
1018                luma_tolerance: 8,
1019                bits_per_pixel: 18,
1020                ordering: ToneOrdering::BaseProximity,
1021            },
1022        ];
1023
1024        let bytes =
1025            encode_toned_carrier_map(&spans, Some(1537)).unwrap();
1026        let decoded =
1027            decode_toned_carrier_map(&bytes, Some(1537)).unwrap();
1028
1029        assert_eq!(decoded, spans);
1030    }
1031
1032    #[test]
1033    fn toned_offsets_are_derived() {
1034        let spans = vec![
1035            ToneSpanDescriptor {
1036                byte_length: 5,
1037                base: [1, 2, 3],
1038                luma_tolerance: 0,
1039                bits_per_pixel: 8,
1040                ordering: ToneOrdering::BaseProximity,
1041            },
1042            ToneSpanDescriptor {
1043                byte_length: 7,
1044                base: [4, 5, 6],
1045                luma_tolerance: 1,
1046                bits_per_pixel: 4,
1047                ordering: ToneOrdering::ChromaProximity,
1048            },
1049        ];
1050
1051        let resolved = resolve_tone_spans(&spans, Some(12)).unwrap();
1052        assert_eq!(resolved[0].byte_offset, 0);
1053        assert_eq!(resolved[1].byte_offset, 5);
1054        assert_eq!(resolved[0].pixel_count, 5);
1055        assert_eq!(resolved[1].pixel_offset, 5);
1056        assert_eq!(resolved[1].pixel_count, 14);
1057    }
1058
1059    #[test]
1060    fn toned_map_rejects_overlong_varuint() {
1061        let bytes = [
1062            TONED_CARRIER_MAP_VERSION,
1063            0,
1064            1,
1065            0x81,
1066            0x00,
1067            0,
1068            0,
1069            0,
1070            0,
1071            8,
1072            TONED_ORDERING_BASE_PROXIMITY,
1073        ];
1074        assert!(decode_toned_carrier_map(&bytes, None).is_err());
1075    }
1076
1077}