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;
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
50#[derive(Debug, Clone, Serialize, Deserialize)]
51#[serde(rename_all = "camelCase")]
52pub struct RecordDescriptor {
53    pub version: u8,
54    pub checksum_protected: bool,
55    pub b_value: f64,
56    pub record_profile: Option<String>,
57    pub stream_byte_length: Option<usize>,
58    pub generation_version: Option<String>,
59    pub payload_encoding: Option<String>,
60    pub title: Option<String>,
61    pub artist: Option<String>,
62    pub release_id: Option<String>,
63    pub catalog_number: Option<String>,
64    pub label: Option<String>,
65    pub artwork_credit: Option<String>,
66    pub license: Option<String>,
67    pub canonical_url: Option<String>,
68    pub created_at: Option<String>,
69    pub arbitrary_metadata: Option<String>,
70    pub signed_release_manifest: Option<String>,
71    pub signature_algorithm: Option<String>,
72    pub signature_key_id: Option<String>,
73    pub signature: Option<String>,
74    pub manifest_sha256: Option<String>,
75    pub stego_sidecar_descriptor: Option<String>,
76    pub chain_registration_receipt: Option<String>,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct DescriptorPrefix {
81    pub version: u8,
82    pub payload_len: usize,
83    pub segment_count: usize,
84    pub segment_stream_len: usize,
85    pub b_value_bits: u64,
86}
87
88pub fn metadata_pixel_count_for_byte_length(byte_length: usize) -> usize {
89    byte_length.saturating_mul(2)
90}
91
92pub fn metadata_byte_capacity_for_pixel_count(pixel_count: usize) -> usize {
93    pixel_count / 2
94}
95
96pub fn metadata_bytes_from_grayscale_rgba(
97    rgba: &[u8],
98    indices: &[usize],
99    byte_length: usize,
100    label: &str,
101) -> Result<Vec<u8>> {
102    let pixel_count = metadata_pixel_count_for_byte_length(byte_length);
103
104    if indices.len() < pixel_count {
105        bail!("{label} spiral capacity is too small");
106    }
107
108    let mut bytes = Vec::with_capacity(byte_length);
109
110    for byte_number in 0..byte_length {
111        let mut nibbles = [0_u8; 2];
112
113        for nibble_index in 0..2 {
114            let pixel_index = indices[byte_number * 2 + nibble_index];
115            let rgba_index = pixel_index * 4;
116
117            if rgba_index + 3 >= rgba.len() {
118                bail!("{label} spiral pixel index is outside RGBA buffer");
119            }
120
121            let red = rgba[rgba_index];
122            let green = rgba[rgba_index + 1];
123            let blue = rgba[rgba_index + 2];
124            let alpha = rgba[rgba_index + 3];
125
126            if alpha == 0 {
127                bail!("{label} spiral pixel is empty");
128            }
129
130            if red != green || green != blue {
131                bail!("{label} metadata pixel is not grayscale");
132            }
133
134            let Some(nibble) = red.checked_sub(METADATA_GRAYSCALE_NIBBLE_BASE) else {
135                bail!("{label} metadata pixel is outside light grayscale encoding");
136            };
137
138            if nibble > 0x0f {
139                bail!("{label} metadata pixel is outside light grayscale encoding");
140            }
141
142            nibbles[nibble_index] = nibble;
143        }
144
145        bytes.push((nibbles[0] << 4) | nibbles[1]);
146    }
147
148    Ok(bytes)
149}
150
151
152pub fn decode_record_descriptor_bytes(bytes: &[u8]) -> Result<RecordDescriptor> {
153    let prefix = decode_descriptor_prefix(bytes)?;
154
155    if prefix.version != RECORD_DESCRIPTOR_VERSION {
156        bail!("record descriptor version mismatch");
157    }
158
159    if prefix.payload_len != RECORD_DESCRIPTOR_PREFIX_LENGTH + prefix.segment_stream_len {
160        bail!("record descriptor segment stream length mismatch");
161    }
162
163    let body = &bytes[RECORD_DESCRIPTOR_PREFIX_LENGTH..prefix.payload_len];
164    let mut offset = 0usize;
165    let mut crc32_range = None;
166    let mut crc32 = None;
167    let mut stream_byte_length = None;
168    let mut generation_version = None;
169    let mut payload_encoding = None;
170    let mut record_profile = None;
171    let mut title = None;
172    let mut artist = None;
173    let mut release_id = None;
174    let mut catalog_number = None;
175    let mut label = None;
176    let mut artwork_credit = None;
177    let mut license = None;
178    let mut canonical_url = None;
179    let mut created_at = None;
180    let mut arbitrary_metadata = None;
181    let mut signed_release_manifest = None;
182    let mut signature_algorithm = None;
183    let mut signature_key_id = None;
184    let mut signature = None;
185    let mut manifest_sha256 = None;
186    let mut stego_sidecar_descriptor = None;
187    let mut chain_registration_receipt = None;
188
189    for _ in 0..prefix.segment_count {
190        if offset + 3 > body.len() {
191            bail!("record descriptor segment is truncated");
192        }
193
194        let kind = body[offset];
195        let len = u16::from_be_bytes(
196            body[offset + 1..offset + 3]
197                .try_into()
198                .expect("slice length"),
199        ) as usize;
200
201        let payload_start = offset + 3;
202        let payload_end = payload_start + len;
203
204        if payload_end > body.len() {
205            bail!("record descriptor segment payload is truncated");
206        }
207
208        let payload = &body[payload_start..payload_end];
209
210        match kind {
211            SEGMENT_DESCRIPTOR_CRC32 => {
212                if crc32.is_some() {
213                    bail!("duplicate record descriptor CRC32 segment");
214                }
215
216                if payload.len() != 4 {
217                    bail!("record descriptor CRC32 segment has invalid length");
218                }
219
220                crc32 = Some(u32::from_be_bytes(
221                    payload.try_into().expect("slice length"),
222                ));
223
224                let absolute_start = RECORD_DESCRIPTOR_PREFIX_LENGTH + payload_start;
225                crc32_range = Some(absolute_start..absolute_start + payload.len());
226            }
227            SEGMENT_STREAM_BYTE_LENGTH => {
228                if payload.len() != 8 {
229                    bail!("record descriptor stream byte length segment has invalid length");
230                }
231
232                let raw_len = u64::from_be_bytes(payload.try_into().expect("slice length"));
233
234                stream_byte_length = if raw_len == STREAM_BYTE_LENGTH_ABSENT {
235                    None
236                } else {
237                    Some(
238                        usize::try_from(raw_len)
239                            .context("record descriptor stream byte length exceeds usize")?,
240                    )
241                };
242            }
243            SEGMENT_GENERATION_VERSION => {
244                generation_version = decode_optional_text(payload, "generation version")?;
245            }
246            SEGMENT_PAYLOAD_ENCODING => {
247                payload_encoding = decode_optional_text(payload, "payload encoding")?;
248            }
249            SEGMENT_RECORD_PROFILE => {
250                record_profile = decode_optional_text(payload, "record profile")?;
251            }
252            SEGMENT_TITLE => {
253                title = decode_optional_text(payload, "title")?;
254            }
255            SEGMENT_ARTIST => {
256                artist = decode_optional_text(payload, "artist")?;
257            }
258            SEGMENT_RELEASE_ID => {
259                release_id = decode_optional_text(payload, "release ID")?;
260            }
261            SEGMENT_CATALOG_NUMBER => {
262                catalog_number = decode_optional_text(payload, "catalog number")?;
263            }
264            SEGMENT_LABEL => {
265                label = decode_optional_text(payload, "label")?;
266            }
267            SEGMENT_ARTWORK_CREDIT => {
268                artwork_credit = decode_optional_text(payload, "artwork credit")?;
269            }
270            SEGMENT_LICENSE => {
271                license = decode_optional_text(payload, "license")?;
272            }
273            SEGMENT_CANONICAL_URL => {
274                canonical_url = decode_optional_text(payload, "canonical URL")?;
275            }
276            SEGMENT_CREATED_AT => {
277                created_at = decode_optional_text(payload, "created-at timestamp")?;
278            }
279            SEGMENT_ARBITRARY_METADATA => {
280                arbitrary_metadata = decode_optional_text(payload, "arbitrary metadata")?;
281            }
282            SEGMENT_ARBITRARY_METADATA_BROTLI => {
283                arbitrary_metadata =
284                    decode_optional_compressed_text(payload, "arbitrary metadata")?;
285            }
286            SEGMENT_SIGNED_RELEASE_MANIFEST => {
287                signed_release_manifest = decode_optional_text(payload, "signed release manifest")?;
288            }
289            SEGMENT_SIGNED_RELEASE_MANIFEST_BROTLI => {
290                signed_release_manifest =
291                    decode_optional_compressed_text(payload, "signed release manifest")?;
292            }
293            SEGMENT_SIGNATURE_ALGORITHM => {
294                signature_algorithm = decode_optional_text(payload, "signature algorithm")?;
295            }
296            SEGMENT_SIGNATURE_KEY_ID => {
297                signature_key_id = decode_optional_text(payload, "signature key ID")?;
298            }
299            SEGMENT_SIGNATURE => {
300                signature = decode_optional_text(payload, "release signature")?;
301            }
302            SEGMENT_MANIFEST_SHA256 => {
303                manifest_sha256 = decode_optional_text(payload, "manifest SHA-256")?;
304            }
305            SEGMENT_STEGO_SIDECAR_DESCRIPTOR => {
306                stego_sidecar_descriptor = Some(general_purpose::STANDARD.encode(payload));
307            }
308            SEGMENT_CHAIN_REGISTRATION_RECEIPT => {
309                chain_registration_receipt =
310                    decode_optional_text(payload, "chain registration receipt")?;
311            }
312            SEGMENT_CHAIN_REGISTRATION_RECEIPT_BROTLI => {
313                chain_registration_receipt =
314                    decode_optional_compressed_text(payload, "chain registration receipt")?;
315            }
316            _ => {}
317        }
318
319        offset = payload_end;
320    }
321
322    if offset != body.len() {
323        bail!("record descriptor segment stream has trailing bytes");
324    }
325
326    let expected = crc32.context("record descriptor CRC32 segment is missing")?;
327    let range = crc32_range.context("record descriptor CRC32 segment is missing")?;
328    let mut canonical = bytes[..prefix.payload_len].to_vec();
329    canonical[range].fill(0);
330
331    let actual = compute_descriptor_crc32(&canonical);
332
333    if actual != expected {
334        bail!("record descriptor CRC32 mismatch");
335    }
336
337    let b_value = f64::from_bits(prefix.b_value_bits);
338
339    if !(b_value.is_finite() && b_value > 0.0) {
340        bail!("decoded invalid b_value");
341    }
342
343    Ok(RecordDescriptor {
344        version: prefix.version,
345        checksum_protected: true,
346        b_value,
347        record_profile,
348        stream_byte_length,
349        generation_version,
350        payload_encoding,
351        title,
352        artist,
353        release_id,
354        catalog_number,
355        label,
356        artwork_credit,
357        license,
358        canonical_url,
359        created_at,
360        arbitrary_metadata,
361        signed_release_manifest,
362        signature_algorithm,
363        signature_key_id,
364        signature,
365        manifest_sha256,
366        stego_sidecar_descriptor,
367        chain_registration_receipt,
368    })
369}
370
371pub fn decode_descriptor_prefix(bytes: &[u8]) -> Result<DescriptorPrefix> {
372    if bytes.len() < RECORD_DESCRIPTOR_PREFIX_LENGTH {
373        bail!("record descriptor payload too short");
374    }
375
376    if &bytes[..4] != RECORD_DESCRIPTOR_MAGIC {
377        bail!("record descriptor magic mismatch");
378    }
379
380    let version = bytes[4];
381    let payload_len = u16::from_be_bytes(bytes[5..7].try_into().expect("slice length")) as usize;
382    let segment_count = u16::from_be_bytes(bytes[7..9].try_into().expect("slice length")) as usize;
383    let segment_stream_len =
384        u16::from_be_bytes(bytes[9..11].try_into().expect("slice length")) as usize;
385    let b_value_bits = u64::from_be_bytes(bytes[11..19].try_into().expect("slice length"));
386
387    if payload_len < RECORD_DESCRIPTOR_PREFIX_LENGTH || payload_len > bytes.len() {
388        bail!("record descriptor payload length is invalid");
389    }
390
391    Ok(DescriptorPrefix {
392        version,
393        payload_len,
394        segment_count,
395        segment_stream_len,
396        b_value_bits,
397    })
398}
399
400pub fn compute_descriptor_crc32(bytes: &[u8]) -> u32 {
401    record_core::crc32_ieee(bytes)
402}
403
404fn brotli_decompress_payload(payload: &[u8], raw_len: usize) -> Result<Vec<u8>> {
405    let mut reader = brotli::Decompressor::new(payload, 4096);
406    let mut out = Vec::with_capacity(raw_len);
407    reader
408        .read_to_end(&mut out)
409        .context("failed to Brotli-decompress record descriptor segment")?;
410
411    if out.len() != raw_len {
412        bail!(
413            "decompressed record descriptor segment length {} does not match declared length {}",
414            out.len(),
415            raw_len
416        );
417    }
418
419    Ok(out)
420}
421
422
423fn decode_optional_text(payload: &[u8], label: &str) -> Result<Option<String>> {
424    if payload.is_empty() {
425        return Ok(None);
426    }
427
428    Ok(Some(String::from_utf8(payload.to_vec()).with_context(
429        || format!("record descriptor {label} is not valid UTF-8"),
430    )?))
431}
432
433fn decode_optional_compressed_text(payload: &[u8], label: &str) -> Result<Option<String>> {
434    if payload.is_empty() {
435        return Ok(None);
436    }
437
438    if payload.len() < 5 {
439        bail!("record descriptor compressed {label} segment is truncated");
440    }
441
442    if payload[0] != RECORD_DESCRIPTOR_COMPRESSION_BROTLI {
443        bail!("record descriptor compressed {label} uses unsupported compression codec");
444    }
445
446    let raw_len = u32::from_be_bytes(payload[1..5].try_into().expect("slice length")) as usize;
447    let decompressed = brotli_decompress_payload(&payload[5..], raw_len)?;
448
449    decode_optional_text(&decompressed, label)
450}