Skip to main content

record_descriptor/
lib.rs

1use anyhow::{bail, Context, Result};
2use base64::{engine::general_purpose, Engine as _};
3use serde::{Deserialize, Serialize};
4use std::io::{Read, Write};
5
6pub const RECORD_DESCRIPTOR_MAGIC: &[u8; 4] = b"BRD1";
7pub const RECORD_DESCRIPTOR_VERSION: u8 = 1;
8pub const RECORD_DESCRIPTOR_PREFIX_LENGTH: usize = 19;
9pub const RECORD_DESCRIPTOR_TEXT_LIMIT: usize = 96;
10pub const RECORD_DESCRIPTOR_CREATOR_METADATA_TEXT_LIMIT: usize = 1024;
11pub const RECORD_DESCRIPTOR_SIGNED_RELEASE_TEXT_LIMIT: usize = 4096;
12pub const RECORD_DESCRIPTOR_CHAIN_RECEIPT_TEXT_LIMIT: usize = 8192;
13pub const RECORD_DESCRIPTOR_COMPRESSION_BROTLI: u8 = 1;
14pub const RECORD_DESCRIPTOR_BROTLI_QUALITY: u32 = 11;
15pub const STREAM_BYTE_LENGTH_ABSENT: u64 = u64::MAX;
16
17pub const METADATA_GRAYSCALE_NIBBLE_BASE: u8 = 120;
18pub const UNUSED_METADATA_GROOVE_RGB_MIN: u8 = 112;
19pub const UNUSED_METADATA_GROOVE_RGB_SPAN: u8 = 32;
20pub const UNUSED_METADATA_GROOVE_ALPHA: u8 = 128;
21pub const UNUSED_METADATA_GROOVE_FADE_TURNS: f64 = 0.5;
22
23pub const SEGMENT_DESCRIPTOR_CRC32: u8 = 1;
24pub const SEGMENT_STREAM_BYTE_LENGTH: u8 = 2;
25pub const SEGMENT_GENERATION_VERSION: u8 = 3;
26pub const SEGMENT_RECORD_PROFILE: u8 = 4;
27pub const SEGMENT_TITLE: u8 = 5;
28pub const SEGMENT_ARTIST: u8 = 6;
29pub const SEGMENT_PAYLOAD_ENCODING: u8 = 7;
30pub const SEGMENT_RELEASE_ID: u8 = 8;
31pub const SEGMENT_CATALOG_NUMBER: u8 = 9;
32pub const SEGMENT_LABEL: u8 = 10;
33pub const SEGMENT_ARTWORK_CREDIT: u8 = 11;
34pub const SEGMENT_LICENSE: u8 = 12;
35pub const SEGMENT_CANONICAL_URL: u8 = 13;
36pub const SEGMENT_CREATED_AT: u8 = 14;
37pub const SEGMENT_ARBITRARY_METADATA: u8 = 15;
38pub const SEGMENT_SIGNED_RELEASE_MANIFEST: u8 = 16;
39pub const SEGMENT_SIGNATURE_ALGORITHM: u8 = 17;
40pub const SEGMENT_SIGNATURE_KEY_ID: u8 = 18;
41pub const SEGMENT_SIGNATURE: u8 = 19;
42pub const SEGMENT_MANIFEST_SHA256: u8 = 20;
43pub const SEGMENT_STEGO_SIDECAR_DESCRIPTOR: u8 = 21;
44pub const SEGMENT_CHAIN_REGISTRATION_RECEIPT: u8 = 22;
45pub const SEGMENT_ARBITRARY_METADATA_BROTLI: u8 = 23;
46pub const SEGMENT_SIGNED_RELEASE_MANIFEST_BROTLI: u8 = 24;
47pub const SEGMENT_CHAIN_REGISTRATION_RECEIPT_BROTLI: u8 = 25;
48
49#[derive(Debug, Clone, Default, Serialize, Deserialize)]
50#[serde(rename_all = "camelCase")]
51pub struct RecordDescriptorInput {
52    pub record_profile: String,
53    pub stream_byte_length: Option<usize>,
54    pub generation_version: Option<String>,
55    pub payload_encoding: Option<String>,
56    pub title: Option<String>,
57    pub artist: Option<String>,
58    pub release_id: Option<String>,
59    pub catalog_number: Option<String>,
60    pub label: Option<String>,
61    pub artwork_credit: Option<String>,
62    pub license: Option<String>,
63    pub canonical_url: Option<String>,
64    pub created_at: Option<String>,
65    pub arbitrary_metadata: Option<String>,
66    pub signed_release_manifest: Option<String>,
67    pub signature_algorithm: Option<String>,
68    pub signature_key_id: Option<String>,
69    pub signature: Option<String>,
70    pub manifest_sha256: Option<String>,
71    pub stego_sidecar_descriptor: Option<Vec<u8>>,
72    pub chain_registration_receipt: Option<String>,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
76#[serde(rename_all = "camelCase")]
77pub struct RecordDescriptor {
78    pub version: u8,
79    pub checksum_protected: bool,
80    pub b_value: f64,
81    pub record_profile: Option<String>,
82    pub stream_byte_length: Option<usize>,
83    pub generation_version: Option<String>,
84    pub payload_encoding: Option<String>,
85    pub title: Option<String>,
86    pub artist: Option<String>,
87    pub release_id: Option<String>,
88    pub catalog_number: Option<String>,
89    pub label: Option<String>,
90    pub artwork_credit: Option<String>,
91    pub license: Option<String>,
92    pub canonical_url: Option<String>,
93    pub created_at: Option<String>,
94    pub arbitrary_metadata: Option<String>,
95    pub signed_release_manifest: Option<String>,
96    pub signature_algorithm: Option<String>,
97    pub signature_key_id: Option<String>,
98    pub signature: Option<String>,
99    pub manifest_sha256: Option<String>,
100    pub stego_sidecar_descriptor: Option<String>,
101    pub chain_registration_receipt: Option<String>,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct DescriptorPrefix {
106    pub version: u8,
107    pub payload_len: usize,
108    pub segment_count: usize,
109    pub segment_stream_len: usize,
110    pub b_value_bits: u64,
111}
112
113pub fn metadata_pixel_count_for_byte_length(byte_length: usize) -> usize {
114    byte_length.saturating_mul(2)
115}
116
117pub fn metadata_byte_capacity_for_pixel_count(pixel_count: usize) -> usize {
118    pixel_count / 2
119}
120
121pub fn metadata_fade_pixel_count(pixel_count: usize, turns: f64) -> usize {
122    if pixel_count == 0 {
123        return 0;
124    }
125
126    let pixels_per_turn = pixel_count as f64 / turns.max(0.01);
127
128    (pixels_per_turn * UNUSED_METADATA_GROOVE_FADE_TURNS)
129        .round()
130        .max(1.0) as usize
131}
132
133pub fn metadata_bytes_from_grayscale_rgba(
134    rgba: &[u8],
135    indices: &[usize],
136    byte_length: usize,
137    label: &str,
138) -> Result<Vec<u8>> {
139    let pixel_count = metadata_pixel_count_for_byte_length(byte_length);
140
141    if indices.len() < pixel_count {
142        bail!("{label} spiral capacity is too small");
143    }
144
145    let mut bytes = Vec::with_capacity(byte_length);
146
147    for byte_number in 0..byte_length {
148        let mut nibbles = [0_u8; 2];
149
150        for nibble_index in 0..2 {
151            let pixel_index = indices[byte_number * 2 + nibble_index];
152            let rgba_index = pixel_index * 4;
153
154            if rgba_index + 3 >= rgba.len() {
155                bail!("{label} spiral pixel index is outside RGBA buffer");
156            }
157
158            let red = rgba[rgba_index];
159            let green = rgba[rgba_index + 1];
160            let blue = rgba[rgba_index + 2];
161            let alpha = rgba[rgba_index + 3];
162
163            if alpha == 0 {
164                bail!("{label} spiral pixel is empty");
165            }
166
167            if red != green || green != blue {
168                bail!("{label} metadata pixel is not grayscale");
169            }
170
171            let Some(nibble) = red.checked_sub(METADATA_GRAYSCALE_NIBBLE_BASE) else {
172                bail!("{label} metadata pixel is outside light grayscale encoding");
173            };
174
175            if nibble > 0x0f {
176                bail!("{label} metadata pixel is outside light grayscale encoding");
177            }
178
179            nibbles[nibble_index] = nibble;
180        }
181
182        bytes.push((nibbles[0] << 4) | nibbles[1]);
183    }
184
185    Ok(bytes)
186}
187
188pub fn paint_metadata_bytes_as_grayscale(
189    data: &mut [u8],
190    indices: &[usize],
191    bytes: &[u8],
192) -> usize {
193    let byte_count = bytes
194        .len()
195        .min(metadata_byte_capacity_for_pixel_count(indices.len()));
196
197    for (byte_number, &byte) in bytes.iter().take(byte_count).enumerate() {
198        let high = METADATA_GRAYSCALE_NIBBLE_BASE + ((byte >> 4) & 0x0f);
199        let low = METADATA_GRAYSCALE_NIBBLE_BASE + (byte & 0x0f);
200
201        for (nibble_index, value) in [high, low].iter().enumerate() {
202            let pixel_index = indices[byte_number * 2 + nibble_index];
203            let rgba_index = pixel_index * 4;
204
205            if rgba_index + 3 >= data.len() {
206                continue;
207            }
208
209            data[rgba_index] = *value;
210            data[rgba_index + 1] = *value;
211            data[rgba_index + 2] = *value;
212            data[rgba_index + 3] = 255;
213        }
214    }
215
216    metadata_pixel_count_for_byte_length(byte_count)
217}
218
219pub fn paint_unused_metadata_groove(
220    data: &mut [u8],
221    indices: &[usize],
222    start_pixel: usize,
223    salt: usize,
224    fade_pixels: usize,
225) {
226    for (pixel_number, &pixel_index) in indices.iter().enumerate().skip(start_pixel) {
227        let rgba_index = pixel_index * 4;
228
229        if rgba_index + 3 >= data.len() {
230            continue;
231        }
232
233        let dither = metadata_dither(pixel_index, pixel_number, salt);
234        let gray = UNUSED_METADATA_GROOVE_RGB_MIN
235            + (dither.rotate_left(1) % (UNUSED_METADATA_GROOVE_RGB_SPAN + 1));
236        let alpha = unused_metadata_alpha(pixel_number, start_pixel, fade_pixels);
237
238        data[rgba_index] = gray;
239        data[rgba_index + 1] = gray;
240        data[rgba_index + 2] = gray;
241        data[rgba_index + 3] = alpha;
242    }
243}
244
245pub fn encode_record_descriptor_stream(
246    b_value: f64,
247    descriptor: &RecordDescriptorInput,
248    byte_capacity: usize,
249) -> Result<Vec<u8>> {
250    if !(b_value.is_finite() && b_value > 0.0) {
251        bail!("A positive finite b_value is required.");
252    }
253
254    let (body, segment_count) = encode_segmented_body(descriptor)?;
255    let payload_len = RECORD_DESCRIPTOR_PREFIX_LENGTH + body.len();
256
257    if payload_len > byte_capacity {
258        bail!("record descriptor exceeds combined lead-in and lead-out capacity");
259    }
260
261    if payload_len > u16::MAX as usize {
262        bail!("record descriptor payload is too large");
263    }
264
265    if body.len() > u16::MAX as usize {
266        bail!("record descriptor segment stream exceeds length limit");
267    }
268
269    let mut full = Vec::with_capacity(payload_len);
270    full.extend_from_slice(RECORD_DESCRIPTOR_MAGIC);
271    full.push(RECORD_DESCRIPTOR_VERSION);
272    full.extend_from_slice(&(payload_len as u16).to_be_bytes());
273    full.extend_from_slice(&segment_count.to_be_bytes());
274    full.extend_from_slice(&(body.len() as u16).to_be_bytes());
275    full.extend_from_slice(&b_value.to_bits().to_be_bytes());
276    full.extend_from_slice(&body);
277
278    let crc32 = compute_descriptor_crc32(&full);
279    full[RECORD_DESCRIPTOR_PREFIX_LENGTH + 3..RECORD_DESCRIPTOR_PREFIX_LENGTH + 7]
280        .copy_from_slice(&crc32.to_be_bytes());
281
282    Ok(full)
283}
284
285pub fn decode_record_descriptor_bytes(bytes: &[u8]) -> Result<RecordDescriptor> {
286    let prefix = decode_descriptor_prefix(bytes)?;
287
288    if prefix.version != RECORD_DESCRIPTOR_VERSION {
289        bail!("record descriptor version mismatch");
290    }
291
292    if prefix.payload_len != RECORD_DESCRIPTOR_PREFIX_LENGTH + prefix.segment_stream_len {
293        bail!("record descriptor segment stream length mismatch");
294    }
295
296    let body = &bytes[RECORD_DESCRIPTOR_PREFIX_LENGTH..prefix.payload_len];
297    let mut offset = 0usize;
298    let mut crc32_range = None;
299    let mut crc32 = None;
300    let mut stream_byte_length = None;
301    let mut generation_version = None;
302    let mut payload_encoding = None;
303    let mut record_profile = None;
304    let mut title = None;
305    let mut artist = None;
306    let mut release_id = None;
307    let mut catalog_number = None;
308    let mut label = None;
309    let mut artwork_credit = None;
310    let mut license = None;
311    let mut canonical_url = None;
312    let mut created_at = None;
313    let mut arbitrary_metadata = None;
314    let mut signed_release_manifest = None;
315    let mut signature_algorithm = None;
316    let mut signature_key_id = None;
317    let mut signature = None;
318    let mut manifest_sha256 = None;
319    let mut stego_sidecar_descriptor = None;
320    let mut chain_registration_receipt = None;
321
322    for _ in 0..prefix.segment_count {
323        if offset + 3 > body.len() {
324            bail!("record descriptor segment is truncated");
325        }
326
327        let kind = body[offset];
328        let len = u16::from_be_bytes(
329            body[offset + 1..offset + 3]
330                .try_into()
331                .expect("slice length"),
332        ) as usize;
333
334        let payload_start = offset + 3;
335        let payload_end = payload_start + len;
336
337        if payload_end > body.len() {
338            bail!("record descriptor segment payload is truncated");
339        }
340
341        let payload = &body[payload_start..payload_end];
342
343        match kind {
344            SEGMENT_DESCRIPTOR_CRC32 => {
345                if crc32.is_some() {
346                    bail!("duplicate record descriptor CRC32 segment");
347                }
348
349                if payload.len() != 4 {
350                    bail!("record descriptor CRC32 segment has invalid length");
351                }
352
353                crc32 = Some(u32::from_be_bytes(
354                    payload.try_into().expect("slice length"),
355                ));
356
357                let absolute_start = RECORD_DESCRIPTOR_PREFIX_LENGTH + payload_start;
358                crc32_range = Some(absolute_start..absolute_start + payload.len());
359            }
360            SEGMENT_STREAM_BYTE_LENGTH => {
361                if payload.len() != 8 {
362                    bail!("record descriptor stream byte length segment has invalid length");
363                }
364
365                let raw_len = u64::from_be_bytes(payload.try_into().expect("slice length"));
366
367                stream_byte_length = if raw_len == STREAM_BYTE_LENGTH_ABSENT {
368                    None
369                } else {
370                    Some(
371                        usize::try_from(raw_len)
372                            .context("record descriptor stream byte length exceeds usize")?,
373                    )
374                };
375            }
376            SEGMENT_GENERATION_VERSION => {
377                generation_version = decode_optional_text(payload, "generation version")?;
378            }
379            SEGMENT_PAYLOAD_ENCODING => {
380                payload_encoding = decode_optional_text(payload, "payload encoding")?;
381            }
382            SEGMENT_RECORD_PROFILE => {
383                record_profile = decode_optional_text(payload, "record profile")?;
384            }
385            SEGMENT_TITLE => {
386                title = decode_optional_text(payload, "title")?;
387            }
388            SEGMENT_ARTIST => {
389                artist = decode_optional_text(payload, "artist")?;
390            }
391            SEGMENT_RELEASE_ID => {
392                release_id = decode_optional_text(payload, "release ID")?;
393            }
394            SEGMENT_CATALOG_NUMBER => {
395                catalog_number = decode_optional_text(payload, "catalog number")?;
396            }
397            SEGMENT_LABEL => {
398                label = decode_optional_text(payload, "label")?;
399            }
400            SEGMENT_ARTWORK_CREDIT => {
401                artwork_credit = decode_optional_text(payload, "artwork credit")?;
402            }
403            SEGMENT_LICENSE => {
404                license = decode_optional_text(payload, "license")?;
405            }
406            SEGMENT_CANONICAL_URL => {
407                canonical_url = decode_optional_text(payload, "canonical URL")?;
408            }
409            SEGMENT_CREATED_AT => {
410                created_at = decode_optional_text(payload, "created-at timestamp")?;
411            }
412            SEGMENT_ARBITRARY_METADATA => {
413                arbitrary_metadata = decode_optional_text(payload, "arbitrary metadata")?;
414            }
415            SEGMENT_ARBITRARY_METADATA_BROTLI => {
416                arbitrary_metadata =
417                    decode_optional_compressed_text(payload, "arbitrary metadata")?;
418            }
419            SEGMENT_SIGNED_RELEASE_MANIFEST => {
420                signed_release_manifest = decode_optional_text(payload, "signed release manifest")?;
421            }
422            SEGMENT_SIGNED_RELEASE_MANIFEST_BROTLI => {
423                signed_release_manifest =
424                    decode_optional_compressed_text(payload, "signed release manifest")?;
425            }
426            SEGMENT_SIGNATURE_ALGORITHM => {
427                signature_algorithm = decode_optional_text(payload, "signature algorithm")?;
428            }
429            SEGMENT_SIGNATURE_KEY_ID => {
430                signature_key_id = decode_optional_text(payload, "signature key ID")?;
431            }
432            SEGMENT_SIGNATURE => {
433                signature = decode_optional_text(payload, "release signature")?;
434            }
435            SEGMENT_MANIFEST_SHA256 => {
436                manifest_sha256 = decode_optional_text(payload, "manifest SHA-256")?;
437            }
438            SEGMENT_STEGO_SIDECAR_DESCRIPTOR => {
439                stego_sidecar_descriptor = Some(general_purpose::STANDARD.encode(payload));
440            }
441            SEGMENT_CHAIN_REGISTRATION_RECEIPT => {
442                chain_registration_receipt =
443                    decode_optional_text(payload, "chain registration receipt")?;
444            }
445            SEGMENT_CHAIN_REGISTRATION_RECEIPT_BROTLI => {
446                chain_registration_receipt =
447                    decode_optional_compressed_text(payload, "chain registration receipt")?;
448            }
449            _ => {}
450        }
451
452        offset = payload_end;
453    }
454
455    if offset != body.len() {
456        bail!("record descriptor segment stream has trailing bytes");
457    }
458
459    let expected = crc32.context("record descriptor CRC32 segment is missing")?;
460    let range = crc32_range.context("record descriptor CRC32 segment is missing")?;
461    let mut canonical = bytes[..prefix.payload_len].to_vec();
462    canonical[range].fill(0);
463
464    let actual = compute_descriptor_crc32(&canonical);
465
466    if actual != expected {
467        bail!("record descriptor CRC32 mismatch");
468    }
469
470    let b_value = f64::from_bits(prefix.b_value_bits);
471
472    if !(b_value.is_finite() && b_value > 0.0) {
473        bail!("decoded invalid b_value");
474    }
475
476    Ok(RecordDescriptor {
477        version: prefix.version,
478        checksum_protected: true,
479        b_value,
480        record_profile,
481        stream_byte_length,
482        generation_version,
483        payload_encoding,
484        title,
485        artist,
486        release_id,
487        catalog_number,
488        label,
489        artwork_credit,
490        license,
491        canonical_url,
492        created_at,
493        arbitrary_metadata,
494        signed_release_manifest,
495        signature_algorithm,
496        signature_key_id,
497        signature,
498        manifest_sha256,
499        stego_sidecar_descriptor,
500        chain_registration_receipt,
501    })
502}
503
504pub fn decode_descriptor_prefix(bytes: &[u8]) -> Result<DescriptorPrefix> {
505    if bytes.len() < RECORD_DESCRIPTOR_PREFIX_LENGTH {
506        bail!("record descriptor payload too short");
507    }
508
509    if &bytes[..4] != RECORD_DESCRIPTOR_MAGIC {
510        bail!("record descriptor magic mismatch");
511    }
512
513    let version = bytes[4];
514    let payload_len = u16::from_be_bytes(bytes[5..7].try_into().expect("slice length")) as usize;
515    let segment_count = u16::from_be_bytes(bytes[7..9].try_into().expect("slice length")) as usize;
516    let segment_stream_len =
517        u16::from_be_bytes(bytes[9..11].try_into().expect("slice length")) as usize;
518    let b_value_bits = u64::from_be_bytes(bytes[11..19].try_into().expect("slice length"));
519
520    if payload_len < RECORD_DESCRIPTOR_PREFIX_LENGTH || payload_len > bytes.len() {
521        bail!("record descriptor payload length is invalid");
522    }
523
524    Ok(DescriptorPrefix {
525        version,
526        payload_len,
527        segment_count,
528        segment_stream_len,
529        b_value_bits,
530    })
531}
532
533pub fn compute_descriptor_crc32(bytes: &[u8]) -> u32 {
534    record_core::crc32_ieee(bytes)
535}
536
537fn encode_segmented_body(descriptor: &RecordDescriptorInput) -> Result<(Vec<u8>, u16)> {
538    let profile_bytes = sanitize_text(Some(&descriptor.record_profile));
539    let generation_bytes = sanitize_text(descriptor.generation_version.as_deref());
540    let payload_encoding_bytes =
541        sanitize_text(descriptor.payload_encoding.as_deref().or(Some("rgb")));
542    let title_bytes = sanitize_text(descriptor.title.as_deref());
543    let artist_bytes = sanitize_text(descriptor.artist.as_deref());
544    let release_id_bytes = sanitize_creator_metadata_text(descriptor.release_id.as_deref());
545    let catalog_number_bytes = sanitize_creator_metadata_text(descriptor.catalog_number.as_deref());
546    let label_bytes = sanitize_creator_metadata_text(descriptor.label.as_deref());
547    let artwork_credit_bytes = sanitize_creator_metadata_text(descriptor.artwork_credit.as_deref());
548    let license_bytes = sanitize_creator_metadata_text(descriptor.license.as_deref());
549    let canonical_url_bytes = sanitize_creator_metadata_text(descriptor.canonical_url.as_deref());
550    let created_at_bytes = sanitize_creator_metadata_text(descriptor.created_at.as_deref());
551    let arbitrary_metadata_bytes =
552        sanitize_creator_metadata_text(descriptor.arbitrary_metadata.as_deref());
553    let signed_release_manifest_bytes = exact_signed_release_text(
554        descriptor.signed_release_manifest.as_deref(),
555        "signed release manifest",
556    )?;
557    let signature_algorithm_bytes = exact_signed_release_text(
558        descriptor.signature_algorithm.as_deref(),
559        "signature algorithm",
560    )?;
561    let signature_key_id_bytes =
562        exact_signed_release_text(descriptor.signature_key_id.as_deref(), "signature key ID")?;
563    let signature_bytes =
564        exact_signed_release_text(descriptor.signature.as_deref(), "release signature")?;
565    let manifest_sha256_bytes =
566        exact_signed_release_text(descriptor.manifest_sha256.as_deref(), "manifest SHA-256")?;
567    let sidecar_bytes = descriptor
568        .stego_sidecar_descriptor
569        .clone()
570        .unwrap_or_default();
571    let chain_registration_receipt_bytes = exact_chain_receipt_text(
572        descriptor.chain_registration_receipt.as_deref(),
573        "chain registration receipt",
574    )?;
575
576    let raw_len = match descriptor.stream_byte_length {
577        Some(length) => u64::try_from(length).context("stream byte length exceeds u64")?,
578        None => STREAM_BYTE_LENGTH_ABSENT,
579    };
580
581    let mut out = Vec::new();
582    let mut segment_count = 0u16;
583
584    push_segment(&mut out, SEGMENT_DESCRIPTOR_CRC32, &0u32.to_be_bytes())?;
585    segment_count += 1;
586    push_segment(&mut out, SEGMENT_STREAM_BYTE_LENGTH, &raw_len.to_be_bytes())?;
587    segment_count += 1;
588    push_segment(&mut out, SEGMENT_RECORD_PROFILE, &profile_bytes)?;
589    segment_count += 1;
590    push_segment(&mut out, SEGMENT_PAYLOAD_ENCODING, &payload_encoding_bytes)?;
591    segment_count += 1;
592
593    let segments = vec![
594        (SEGMENT_GENERATION_VERSION, generation_bytes),
595        (SEGMENT_TITLE, title_bytes),
596        (SEGMENT_ARTIST, artist_bytes),
597        (SEGMENT_RELEASE_ID, release_id_bytes),
598        (SEGMENT_CATALOG_NUMBER, catalog_number_bytes),
599        (SEGMENT_LABEL, label_bytes),
600        (SEGMENT_ARTWORK_CREDIT, artwork_credit_bytes),
601        (SEGMENT_LICENSE, license_bytes),
602        (SEGMENT_CANONICAL_URL, canonical_url_bytes),
603        (SEGMENT_CREATED_AT, created_at_bytes),
604        compressed_segment(
605            SEGMENT_ARBITRARY_METADATA,
606            SEGMENT_ARBITRARY_METADATA_BROTLI,
607            arbitrary_metadata_bytes,
608        )?,
609        compressed_segment(
610            SEGMENT_SIGNED_RELEASE_MANIFEST,
611            SEGMENT_SIGNED_RELEASE_MANIFEST_BROTLI,
612            signed_release_manifest_bytes,
613        )?,
614        (SEGMENT_SIGNATURE_ALGORITHM, signature_algorithm_bytes),
615        (SEGMENT_SIGNATURE_KEY_ID, signature_key_id_bytes),
616        (SEGMENT_SIGNATURE, signature_bytes),
617        (SEGMENT_MANIFEST_SHA256, manifest_sha256_bytes),
618        (SEGMENT_STEGO_SIDECAR_DESCRIPTOR, sidecar_bytes),
619        compressed_segment(
620            SEGMENT_CHAIN_REGISTRATION_RECEIPT,
621            SEGMENT_CHAIN_REGISTRATION_RECEIPT_BROTLI,
622            chain_registration_receipt_bytes,
623        )?,
624    ];
625
626    for (kind, payload) in segments {
627        if !payload.is_empty() {
628            push_segment(&mut out, kind, &payload)?;
629            segment_count += 1;
630        }
631    }
632
633    Ok((out, segment_count))
634}
635
636fn push_segment(out: &mut Vec<u8>, kind: u8, payload: &[u8]) -> Result<()> {
637    if payload.len() > u16::MAX as usize {
638        bail!("record descriptor segment {kind} exceeds length limit");
639    }
640
641    out.push(kind);
642    out.extend_from_slice(&(payload.len() as u16).to_be_bytes());
643    out.extend_from_slice(payload);
644
645    Ok(())
646}
647
648fn compressed_segment(
649    raw_kind: u8,
650    compressed_kind: u8,
651    payload: Vec<u8>,
652) -> Result<(u8, Vec<u8>)> {
653    if payload.is_empty() {
654        return Ok((raw_kind, payload));
655    }
656
657    let compressed = brotli_compress_payload(&payload)?;
658    let framed = compressed_payload_frame(payload.len(), &compressed)?;
659
660    if framed.len() < payload.len() {
661        Ok((compressed_kind, framed))
662    } else {
663        Ok((raw_kind, payload))
664    }
665}
666
667fn compressed_payload_frame(raw_len: usize, compressed: &[u8]) -> Result<Vec<u8>> {
668    let raw_len =
669        u32::try_from(raw_len).context("compressed record descriptor payload too large")?;
670    let mut out = Vec::with_capacity(5 + compressed.len());
671    out.push(RECORD_DESCRIPTOR_COMPRESSION_BROTLI);
672    out.extend_from_slice(&raw_len.to_be_bytes());
673    out.extend_from_slice(compressed);
674    Ok(out)
675}
676
677fn brotli_compress_payload(payload: &[u8]) -> Result<Vec<u8>> {
678    let mut out = Vec::new();
679    {
680        let mut writer =
681            brotli::CompressorWriter::new(&mut out, 4096, RECORD_DESCRIPTOR_BROTLI_QUALITY, 22);
682        writer
683            .write_all(payload)
684            .context("failed to Brotli-compress record descriptor segment")?;
685    }
686    Ok(out)
687}
688
689fn brotli_decompress_payload(payload: &[u8], raw_len: usize) -> Result<Vec<u8>> {
690    let mut reader = brotli::Decompressor::new(payload, 4096);
691    let mut out = Vec::with_capacity(raw_len);
692    reader
693        .read_to_end(&mut out)
694        .context("failed to Brotli-decompress record descriptor segment")?;
695
696    if out.len() != raw_len {
697        bail!(
698            "decompressed record descriptor segment length {} does not match declared length {}",
699            out.len(),
700            raw_len
701        );
702    }
703
704    Ok(out)
705}
706
707fn sanitize_text_with_limit(value: Option<&str>, limit: usize) -> Vec<u8> {
708    let Some(value) = value else {
709        return Vec::new();
710    };
711
712    value
713        .trim()
714        .chars()
715        .filter(|ch| !ch.is_control())
716        .collect::<String>()
717        .into_bytes()
718        .into_iter()
719        .take(limit)
720        .collect()
721}
722
723fn sanitize_text(value: Option<&str>) -> Vec<u8> {
724    sanitize_text_with_limit(value, RECORD_DESCRIPTOR_TEXT_LIMIT)
725}
726
727fn sanitize_creator_metadata_text(value: Option<&str>) -> Vec<u8> {
728    sanitize_text_with_limit(value, RECORD_DESCRIPTOR_CREATOR_METADATA_TEXT_LIMIT)
729}
730
731fn exact_signed_release_text(value: Option<&str>, label: &str) -> Result<Vec<u8>> {
732    let Some(value) = value else {
733        return Ok(Vec::new());
734    };
735
736    let bytes = value.as_bytes();
737
738    if bytes.len() > RECORD_DESCRIPTOR_SIGNED_RELEASE_TEXT_LIMIT {
739        bail!("{label} exceeds record descriptor signed release length limit");
740    }
741
742    if value.chars().any(char::is_control) {
743        bail!("{label} must not contain raw control characters");
744    }
745
746    Ok(bytes.to_vec())
747}
748
749fn exact_chain_receipt_text(value: Option<&str>, label: &str) -> Result<Vec<u8>> {
750    let Some(value) = value else {
751        return Ok(Vec::new());
752    };
753
754    if value.len() > RECORD_DESCRIPTOR_CHAIN_RECEIPT_TEXT_LIMIT {
755        bail!("{label} exceeds length limit");
756    }
757
758    Ok(value.as_bytes().to_vec())
759}
760
761fn decode_optional_text(payload: &[u8], label: &str) -> Result<Option<String>> {
762    if payload.is_empty() {
763        return Ok(None);
764    }
765
766    Ok(Some(String::from_utf8(payload.to_vec()).with_context(
767        || format!("record descriptor {label} is not valid UTF-8"),
768    )?))
769}
770
771fn decode_optional_compressed_text(payload: &[u8], label: &str) -> Result<Option<String>> {
772    if payload.is_empty() {
773        return Ok(None);
774    }
775
776    if payload.len() < 5 {
777        bail!("record descriptor compressed {label} segment is truncated");
778    }
779
780    if payload[0] != RECORD_DESCRIPTOR_COMPRESSION_BROTLI {
781        bail!("record descriptor compressed {label} uses unsupported compression codec");
782    }
783
784    let raw_len = u32::from_be_bytes(payload[1..5].try_into().expect("slice length")) as usize;
785    let decompressed = brotli_decompress_payload(&payload[5..], raw_len)?;
786
787    decode_optional_text(&decompressed, label)
788}
789
790fn unused_metadata_alpha(pixel_number: usize, start_pixel: usize, fade_pixels: usize) -> u8 {
791    let offset = pixel_number.saturating_sub(start_pixel);
792
793    if fade_pixels == 0 || offset >= fade_pixels {
794        return UNUSED_METADATA_GROOVE_ALPHA;
795    }
796
797    let t = ((offset + 1) as f64 / fade_pixels as f64).clamp(0.0, 1.0);
798    let eased = t * t * (3.0 - (2.0 * t));
799    let alpha = 255.0 - ((255.0 - f64::from(UNUSED_METADATA_GROOVE_ALPHA)) * eased);
800
801    alpha
802        .round()
803        .clamp(f64::from(UNUSED_METADATA_GROOVE_ALPHA), 255.0) as u8
804}
805
806fn metadata_dither(pixel_index: usize, sequence_index: usize, salt: usize) -> u8 {
807    let mut value = pixel_index as u64;
808    value ^= (sequence_index as u64).wrapping_mul(0x9e37_79b9_7f4a_7c15);
809    value ^= (salt as u64).wrapping_mul(0xbf58_476d_1ce4_e5b9);
810    value ^= value >> 30;
811    value = value.wrapping_mul(0xbf58_476d_1ce4_e5b9);
812    value ^= value >> 27;
813    value = value.wrapping_mul(0x94d0_49bb_1331_11eb);
814    value ^= value >> 31;
815    (value & 0xff) as u8
816}
817
818#[cfg(test)]
819mod tests {
820    use super::*;
821
822    fn segment_kinds(bytes: &[u8]) -> Vec<u8> {
823        let prefix = decode_descriptor_prefix(bytes).unwrap();
824        let body = &bytes[RECORD_DESCRIPTOR_PREFIX_LENGTH..prefix.payload_len];
825        let mut offset = 0usize;
826        let mut kinds = Vec::new();
827
828        for _ in 0..prefix.segment_count {
829            let kind = body[offset];
830            let len = u16::from_be_bytes(body[offset + 1..offset + 3].try_into().unwrap()) as usize;
831            kinds.push(kind);
832            offset += 3 + len;
833        }
834
835        kinds
836    }
837
838    #[test]
839    fn descriptor_round_trips_required_and_optional_segments() {
840        let input = RecordDescriptorInput {
841            record_profile: "single45".to_string(),
842            stream_byte_length: Some(12345),
843            generation_version: Some("build-1".to_string()),
844            payload_encoding: Some("rgb".to_string()),
845            title: Some("Westside".to_string()),
846            artist: Some("Lori Asha".to_string()),
847            release_id: Some("rel-1".to_string()),
848            canonical_url: Some("https://bitneedle.com/r/rel-1".to_string()),
849            signed_release_manifest: Some(r#"{"v":1}"#.to_string()),
850            signature_algorithm: Some("ed25519".to_string()),
851            signature_key_id: Some("key-1".to_string()),
852            signature: Some("sig".to_string()),
853            manifest_sha256: Some("hash".to_string()),
854            chain_registration_receipt: Some(
855                r#"{"v":1,"type":"bitneedle.chainReceipts","releaseHash":"sha256:test","receipts":[]}"#
856                    .to_string(),
857            ),
858            ..RecordDescriptorInput::default()
859        };
860
861        let bytes = encode_record_descriptor_stream(0.25, &input, 4096).unwrap();
862        let decoded = decode_record_descriptor_bytes(&bytes).unwrap();
863
864        assert_eq!(&bytes[..4], b"BRD1");
865        assert_eq!(decoded.version, RECORD_DESCRIPTOR_VERSION);
866        assert!(decoded.checksum_protected);
867        assert_eq!(decoded.b_value, 0.25);
868        assert_eq!(decoded.record_profile.as_deref(), Some("single45"));
869        assert_eq!(decoded.stream_byte_length, Some(12345));
870        assert_eq!(decoded.generation_version.as_deref(), Some("build-1"));
871        assert_eq!(decoded.payload_encoding.as_deref(), Some("rgb"));
872        assert_eq!(decoded.title.as_deref(), Some("Westside"));
873        assert_eq!(decoded.artist.as_deref(), Some("Lori Asha"));
874        assert_eq!(decoded.release_id.as_deref(), Some("rel-1"));
875        assert_eq!(
876            decoded.canonical_url.as_deref(),
877            Some("https://bitneedle.com/r/rel-1")
878        );
879        assert_eq!(
880            decoded.signed_release_manifest.as_deref(),
881            Some(r#"{"v":1}"#)
882        );
883        assert_eq!(decoded.signature_algorithm.as_deref(), Some("ed25519"));
884        assert_eq!(decoded.signature_key_id.as_deref(), Some("key-1"));
885        assert_eq!(decoded.signature.as_deref(), Some("sig"));
886        assert_eq!(decoded.manifest_sha256.as_deref(), Some("hash"));
887        assert_eq!(
888            decoded.chain_registration_receipt.as_deref(),
889            Some(
890                r#"{"v":1,"type":"bitneedle.chainReceipts","releaseHash":"sha256:test","receipts":[]}"#
891            )
892        );
893    }
894
895    #[test]
896    fn descriptor_rejects_crc32_corruption() {
897        let input = RecordDescriptorInput {
898            record_profile: "lp".to_string(),
899            stream_byte_length: Some(10),
900            payload_encoding: Some("rgb".to_string()),
901            ..RecordDescriptorInput::default()
902        };
903
904        let mut bytes = encode_record_descriptor_stream(0.5, &input, 4096).unwrap();
905        let last = bytes.len() - 1;
906        bytes[last] ^= 0x01;
907
908        let err = decode_record_descriptor_bytes(&bytes)
909            .unwrap_err()
910            .to_string();
911
912        assert!(err.contains("CRC32 mismatch"));
913    }
914
915    #[test]
916    fn descriptor_compresses_large_optional_text_segments() {
917        let arbitrary_metadata = format!(
918            r#"{{"credits":["{}"],"notes":"{}"}}"#,
919            "Lori Asha; Wavey.ai; Bitneedle; ".repeat(10),
920            "record revolution rights ".repeat(10)
921        );
922        let signed_release_manifest = format!(
923            r#"{{"v":1,"type":"bitneedle.release","releaseId":"rel-1","revolutions":[{}]}}"#,
924            (0..60)
925                .map(|index| format!(r#"{{"i":{index},"sha256":"{}"}}"#, "a".repeat(43)))
926                .collect::<Vec<_>>()
927                .join(",")
928        );
929        let chain_registration_receipt = format!(
930            r#"{{"v":1,"type":"bitneedle.chainReceipts","releaseHash":"sha256:{}","receipts":[{}]}}"#,
931            "b".repeat(43),
932            (0..20)
933                .map(|index| {
934                    format!(
935                        r#"{{"chain":"base","chainId":84532,"network":"base-sepolia","transactionHash":"0x{}","blockNumber":{}}}"#,
936                        "c".repeat(64),
937                        1000 + index
938                    )
939                })
940                .collect::<Vec<_>>()
941                .join(",")
942        );
943
944        let input = RecordDescriptorInput {
945            record_profile: "lp".to_string(),
946            stream_byte_length: Some(10),
947            payload_encoding: Some("rgb".to_string()),
948            arbitrary_metadata: Some(arbitrary_metadata.clone()),
949            signed_release_manifest: Some(signed_release_manifest.clone()),
950            chain_registration_receipt: Some(chain_registration_receipt.clone()),
951            ..RecordDescriptorInput::default()
952        };
953
954        let bytes = encode_record_descriptor_stream(0.5, &input, 8192).unwrap();
955        let kinds = segment_kinds(&bytes);
956        let decoded = decode_record_descriptor_bytes(&bytes).unwrap();
957
958        assert!(kinds.contains(&SEGMENT_ARBITRARY_METADATA_BROTLI));
959        assert!(kinds.contains(&SEGMENT_SIGNED_RELEASE_MANIFEST_BROTLI));
960        assert!(kinds.contains(&SEGMENT_CHAIN_REGISTRATION_RECEIPT_BROTLI));
961        assert_eq!(
962            decoded.arbitrary_metadata.as_deref(),
963            Some(arbitrary_metadata.as_str())
964        );
965        assert_eq!(
966            decoded.signed_release_manifest.as_deref(),
967            Some(signed_release_manifest.as_str())
968        );
969        assert_eq!(
970            decoded.chain_registration_receipt.as_deref(),
971            Some(chain_registration_receipt.as_str())
972        );
973    }
974
975    #[test]
976    fn descriptor_rejects_b_value_corruption() {
977        let input = RecordDescriptorInput {
978            record_profile: "lp".to_string(),
979            stream_byte_length: Some(10),
980            payload_encoding: Some("rgb".to_string()),
981            ..RecordDescriptorInput::default()
982        };
983
984        let mut bytes = encode_record_descriptor_stream(0.5, &input, 4096).unwrap();
985        bytes[18] ^= 0x01;
986
987        let err = decode_record_descriptor_bytes(&bytes)
988            .unwrap_err()
989            .to_string();
990
991        assert!(err.contains("CRC32 mismatch"));
992    }
993
994    #[test]
995    fn grayscale_metadata_round_trips() {
996        let source = b"BRD1";
997        let indices = vec![0, 1, 2, 3, 4, 5, 6, 7];
998        let mut rgba = vec![0u8; 8 * 4];
999
1000        let pixels = paint_metadata_bytes_as_grayscale(&mut rgba, &indices, source);
1001
1002        assert_eq!(pixels, 8);
1003
1004        let decoded =
1005            metadata_bytes_from_grayscale_rgba(&rgba, &indices, source.len(), "test").unwrap();
1006
1007        assert_eq!(decoded, source);
1008    }
1009
1010    #[test]
1011    fn rejects_wrong_magic() {
1012        let input = RecordDescriptorInput {
1013            record_profile: "single45".to_string(),
1014            stream_byte_length: Some(10),
1015            payload_encoding: Some("rgb".to_string()),
1016            ..RecordDescriptorInput::default()
1017        };
1018
1019        let mut bytes = encode_record_descriptor_stream(0.25, &input, 4096).unwrap();
1020
1021        bytes[..4].copy_from_slice(b"BTB2");
1022
1023        let err = decode_record_descriptor_bytes(&bytes)
1024            .unwrap_err()
1025            .to_string();
1026
1027        assert!(err.contains("magic mismatch"));
1028    }
1029}