Skip to main content

record_sidecar/
lib.rs

1use anyhow::{bail, Context, Result};
2use base64::{engine::general_purpose, Engine as _};
3use serde::{Deserialize, Serialize};
4use sha2::{Digest, Sha256};
5use std::io::Read;
6
7pub const SIDECAR_MAGIC: &[u8; 4] = b"BTS1";
8pub const SIDECAR_CONTAINER_VERSION: u8 = 1;
9pub const SIDECAR_POINTER_VERSION: u8 = 1;
10pub const SIDECAR_POINTER_LENGTH: usize = 48;
11pub const SIDECAR_POINTER_SCHEME_PAIRSIGN_SAFE_LUMA_V2: u8 = 1;
12pub const SIDECAR_POINTER_CARRIER_LABEL: u8 = 0x01;
13pub const SIDECAR_POINTER_CARRIER_INTERGROOVE: u8 = 0x02;
14pub const SIDECAR_POINTER_CARRIER_LEAD_IN_DEADWAX: u8 = 0x04;
15pub const SIDECAR_SCHEME_PAIRSIGN_SAFE_LUMA_V2: &str = "pairsign-safe-luma-v2";
16pub const SIDECAR_DEFAULT_SEED: u32 = 0x4b50_4752;
17pub const SIDECAR_PAIR_SIGN_DELTA: i16 = 4;
18pub const SIDECAR_PAIR_MAGNITUDE_DELTA: i16 = 12;
19pub const SIDECAR_PAIR_MAGNITUDE_THRESHOLD: f64 = 16.0;
20pub const SIDECAR_SAFE_V2_MIN_SCORE: u16 = 20;
21pub const SIDECAR_TYPE_OPAQUE: u8 = 0;
22pub const SIDECAR_TYPE_UTF8_TEXT: u8 = 1;
23pub const SIDECAR_TYPE_IMAGE: u8 = 2;
24pub const SIDECAR_TYPE_JSON: u8 = 3;
25pub const SIDECAR_CODEC_RAW: u8 = 0;
26pub const SIDECAR_CODEC_BROTLI: u8 = 1;
27pub const SIDECAR_CODEC_ZSTD: u8 = 2;
28pub const SIDECAR_CODEC_AVIF: u8 = 3;
29pub const SIDECAR_RAW_LENGTH_ABSENT: u32 = u32::MAX;
30pub const DISPLAY_HEADER_MAGIC: &[u8; 4] = b"BDH1";
31pub const DISPLAY_HEADER_VERSION: u8 = 1;
32pub const DISPLAY_HEADER_LENGTH: usize = 128;
33pub const DISPLAY_HEADER_NAME: &str = "bitneedle-display-header.bin";
34pub const DISPLAY_HEADER_MIME: &str = "application/vnd.bitneedle.display-header";
35pub const PACKAGE_METADATA_ITEM_NAME: &str = "bitneedle-package-metadata.json";
36pub const PACKAGE_METADATA_MIME: &str = "application/vnd.bitneedle.package-metadata+json";
37pub const PACKAGE_PHOTO_MIME: &str = "image/avif";
38pub const PACKAGE_COVER_ITEM_NAME: &str = "album-cover.avif";
39pub const PACKAGE_PATTERN_SIDECAR_ITEM_NAME: &str = "bitneedle-pattern-map";
40pub const PACKAGE_PATTERN_SIDECAR_MIME: &str = "application/vnd.bitneedle.pattern-map";
41
42
43#[derive(Debug, Clone, Serialize)]
44#[serde(rename_all = "camelCase")]
45pub struct SidecarContainerValidation {
46    pub ok: bool,
47    pub version: u8,
48    pub flags: u8,
49    pub item_count: usize,
50    pub total_length: usize,
51    pub items: Vec<SidecarItemValidation>,
52}
53
54#[derive(Debug, Clone, Serialize)]
55#[serde(rename_all = "camelCase")]
56pub struct SidecarItemValidation {
57    pub item_type: u8,
58    pub item_type_name: String,
59    pub codec: u8,
60    pub codec_name: String,
61    pub flags: u8,
62    pub raw_byte_length: Option<u32>,
63    pub stored_byte_length: u32,
64    pub name: String,
65    pub mime: String,
66}
67
68#[derive(Debug, Clone, Serialize)]
69#[serde(rename_all = "camelCase")]
70pub struct SidecarDecodedItems {
71    pub ok: bool,
72    pub validation: SidecarContainerValidation,
73    pub items: Vec<SidecarDecodedItem>,
74}
75
76#[derive(Debug, Clone, Serialize)]
77#[serde(rename_all = "camelCase")]
78pub struct SidecarDecodedItem {
79    pub item_type: u8,
80    pub item_type_name: String,
81    pub codec: u8,
82    pub codec_name: String,
83    pub flags: u8,
84    pub raw_byte_length: Option<u32>,
85    pub stored_byte_length: u32,
86    pub decoded_byte_length: usize,
87    pub name: String,
88    pub mime: String,
89    pub stored_data_base64: String,
90    pub data_base64: String,
91    pub text: Option<String>,
92    pub json: Option<serde_json::Value>,
93}
94
95#[derive(Debug, Clone, Serialize)]
96#[serde(rename_all = "camelCase")]
97pub struct SidecarDecodeResult {
98    pub ok: bool,
99    pub descriptor: serde_json::Value,
100    pub validation: SidecarContainerValidation,
101    pub bts1_byte_length: usize,
102    pub sha256: String,
103    pub carrier_pixels: usize,
104    pub carrier_pairs: usize,
105    pub capacity_bytes: usize,
106}
107
108#[derive(Debug, Clone, Serialize)]
109#[serde(rename_all = "camelCase")]
110pub struct SidecarCapacity {
111    pub scheme: String,
112    pub carriers: Vec<String>,
113    pub carrier_pixels: usize,
114    pub carrier_pairs: usize,
115    pub capacity_bits: usize,
116    pub capacity_bytes: usize,
117    pub bits_per_pair: f64,
118    pub two_bit_pairs: usize,
119}
120
121#[derive(Debug, Clone)]
122pub struct SidecarHeaderPointer {
123    pub scheme: String,
124    pub carriers: Vec<SidecarCarrier>,
125    pub seed: u32,
126    pub length: usize,
127    pub sha256: String,
128    pub sha256_bytes: [u8; 32],
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
132#[serde(rename_all = "camelCase")]
133pub enum SidecarCarrier {
134    Label,
135    Intergroove,
136    LeadInDeadwax,
137}
138
139impl SidecarCarrier {
140    pub fn name(self) -> &'static str {
141        sidecar_carrier_name(self)
142    }
143}
144
145pub fn sha256_digest_bytes(bytes: &[u8]) -> [u8; 32] {
146    let digest = Sha256::digest(bytes);
147    let mut out = [0u8; 32];
148    out.copy_from_slice(&digest);
149    out
150}
151
152pub fn sha256_base64url(bytes: &[u8]) -> String {
153    general_purpose::URL_SAFE_NO_PAD.encode(sha256_digest_bytes(bytes))
154}
155
156pub fn decode_base64_text(value: &str, label: &str) -> Result<Vec<u8>> {
157    let trimmed = value.trim();
158    general_purpose::URL_SAFE_NO_PAD
159        .decode(trimmed)
160        .or_else(|_| general_purpose::URL_SAFE.decode(trimmed))
161        .or_else(|_| general_purpose::STANDARD.decode(trimmed))
162        .with_context(|| format!("{label} is not valid base64"))
163}
164
165pub fn sidecar_type_name(item_type: u8) -> String {
166    match item_type {
167        SIDECAR_TYPE_OPAQUE => "opaque".to_string(),
168        SIDECAR_TYPE_UTF8_TEXT => "utf8Text".to_string(),
169        SIDECAR_TYPE_IMAGE => "image".to_string(),
170        SIDECAR_TYPE_JSON => "json".to_string(),
171        value => format!("private:{value}"),
172    }
173}
174
175pub fn sidecar_codec_name(codec: u8) -> String {
176    match codec {
177        SIDECAR_CODEC_RAW => "raw".to_string(),
178        SIDECAR_CODEC_BROTLI => "brotli".to_string(),
179        SIDECAR_CODEC_ZSTD => "zstd".to_string(),
180        SIDECAR_CODEC_AVIF => "avif".to_string(),
181        value => format!("private:{value}"),
182    }
183}
184
185fn parse_sidecar_registry_value(
186    value: &serde_json::Value,
187    label: &str,
188    names: &[(&str, u8)],
189) -> Result<u8> {
190    if let Some(number) = value.as_u64() {
191        return u8::try_from(number).with_context(|| format!("{label} exceeds u8 range"));
192    }
193    let Some(raw) = value.as_str() else {
194        bail!("{label} must be a string or integer");
195    };
196    let normalized = raw.trim().to_ascii_lowercase().replace([' ', '-', '_'], "");
197    for (name, code) in names {
198        if normalized == *name {
199            return Ok(*code);
200        }
201    }
202    bail!("unknown {label}: {raw}");
203}
204
205pub fn parse_sidecar_item_type(value: &serde_json::Value) -> Result<u8> {
206    parse_sidecar_registry_value(
207        value,
208        "sidecar item type",
209        &[
210            ("opaque", SIDECAR_TYPE_OPAQUE),
211            ("bytes", SIDECAR_TYPE_OPAQUE),
212            ("binary", SIDECAR_TYPE_OPAQUE),
213            ("utf8text", SIDECAR_TYPE_UTF8_TEXT),
214            ("text", SIDECAR_TYPE_UTF8_TEXT),
215            ("utf8", SIDECAR_TYPE_UTF8_TEXT),
216            ("image", SIDECAR_TYPE_IMAGE),
217            ("photo", SIDECAR_TYPE_IMAGE),
218            ("json", SIDECAR_TYPE_JSON),
219        ],
220    )
221}
222
223pub fn parse_sidecar_codec(value: &serde_json::Value) -> Result<u8> {
224    parse_sidecar_registry_value(
225        value,
226        "sidecar codec",
227        &[
228            ("raw", SIDECAR_CODEC_RAW),
229            ("none", SIDECAR_CODEC_RAW),
230            ("brotli", SIDECAR_CODEC_BROTLI),
231            ("br", SIDECAR_CODEC_BROTLI),
232            ("zstd", SIDECAR_CODEC_ZSTD),
233            ("zstandard", SIDECAR_CODEC_ZSTD),
234            ("avif", SIDECAR_CODEC_AVIF),
235        ],
236    )
237}
238
239pub fn validate_sidecar_registry_ranges(item_type: u8, codec: u8) -> Result<()> {
240    if (4..=31).contains(&item_type) {
241        bail!("sidecar item type {item_type} is reserved");
242    }
243    if (4..=31).contains(&codec) {
244        bail!("sidecar codec {codec} is reserved");
245    }
246    Ok(())
247}
248
249pub fn validate_sidecar_name(value: &str, label: &str) -> Result<()> {
250    if value.chars().any(|ch| {
251        let code = ch as u32;
252        code <= 0x1f || code == 0x7f
253    }) {
254        bail!("sidecar {label} must not contain control characters");
255    }
256    Ok(())
257}
258
259pub fn looks_like_avif(bytes: &[u8]) -> bool {
260    if bytes.len() < 16 || &bytes[4..8] != b"ftyp" {
261        return false;
262    }
263    bytes[8..]
264        .chunks(4)
265        .any(|chunk| chunk == b"avif" || chunk == b"avis")
266}
267
268pub fn validate_sidecar_item_payload(
269    item_type: u8,
270    codec: u8,
271    raw_byte_length: u32,
272    stored: &[u8],
273    mime: &str,
274) -> Result<()> {
275    validate_sidecar_registry_ranges(item_type, codec)?;
276    if codec == SIDECAR_CODEC_AVIF && item_type != SIDECAR_TYPE_IMAGE {
277        bail!("AVIF sidecar codec is only valid for image items");
278    }
279    if item_type == SIDECAR_TYPE_IMAGE && codec != SIDECAR_CODEC_AVIF {
280        bail!("image sidecar items must use AVIF in this version");
281    }
282    if matches!(item_type, SIDECAR_TYPE_UTF8_TEXT | SIDECAR_TYPE_JSON)
283        && codec == SIDECAR_CODEC_AVIF
284    {
285        bail!("text and JSON sidecar items cannot use AVIF");
286    }
287    if codec == SIDECAR_CODEC_RAW && raw_byte_length != stored.len() as u32 {
288        bail!("raw sidecar item rawByteLength must equal stored length");
289    }
290    if item_type == SIDECAR_TYPE_UTF8_TEXT && codec == SIDECAR_CODEC_RAW {
291        std::str::from_utf8(stored).context("raw UTF-8 text sidecar item is not valid UTF-8")?;
292    }
293    if item_type == SIDECAR_TYPE_JSON && codec == SIDECAR_CODEC_RAW {
294        serde_json::from_slice::<serde_json::Value>(stored)
295            .context("raw JSON sidecar item is not valid JSON")?;
296    }
297    if item_type == SIDECAR_TYPE_IMAGE && codec == SIDECAR_CODEC_AVIF {
298        if !looks_like_avif(stored) {
299            bail!("AVIF sidecar image does not look like an AVIF file");
300        }
301        if !mime.is_empty() && !mime.eq_ignore_ascii_case("image/avif") {
302            bail!("AVIF sidecar image MIME type must be image/avif");
303        }
304    }
305    Ok(())
306}
307
308pub fn default_sidecar_mime(item_type: u8, codec: u8) -> &'static str {
309    match (item_type, codec) {
310        (SIDECAR_TYPE_UTF8_TEXT, _) => "text/plain;charset=utf-8",
311        (SIDECAR_TYPE_JSON, _) => "application/json",
312        (SIDECAR_TYPE_IMAGE, SIDECAR_CODEC_AVIF) => "image/avif",
313        _ => "",
314    }
315}
316
317fn read_u16be(bytes: &[u8], offset: usize, label: &str) -> Result<u16> {
318    if offset + 2 > bytes.len() {
319        bail!("{label} is truncated");
320    }
321    Ok(u16::from_be_bytes(
322        bytes[offset..offset + 2].try_into().expect("slice length"),
323    ))
324}
325
326fn read_u32be(bytes: &[u8], offset: usize, label: &str) -> Result<u32> {
327    if offset + 4 > bytes.len() {
328        bail!("{label} is truncated");
329    }
330    Ok(u32::from_be_bytes(
331        bytes[offset..offset + 4].try_into().expect("slice length"),
332    ))
333}
334
335pub fn validate_sidecar_container(bytes: &[u8]) -> Result<SidecarContainerValidation> {
336    if bytes.len() < 12 {
337        bail!("sidecar container is too short");
338    }
339    if &bytes[..4] != SIDECAR_MAGIC {
340        bail!("sidecar container magic is unsupported");
341    }
342    let version = bytes[4];
343    if version != SIDECAR_CONTAINER_VERSION {
344        bail!("unsupported sidecar container version {version}");
345    }
346    let flags = bytes[5];
347    if flags != 0 {
348        bail!("sidecar container flags must be 0 in this version");
349    }
350    let item_count = read_u16be(bytes, 6, "sidecar item count")? as usize;
351    let total_length = read_u32be(bytes, 8, "sidecar total length")? as usize;
352    if total_length != bytes.len() {
353        bail!("sidecar total length does not match byte stream length");
354    }
355
356    let mut offset = 12usize;
357    let mut items = Vec::with_capacity(item_count);
358    for _ in 0..item_count {
359        if offset + 16 > bytes.len() {
360            bail!("sidecar item header is truncated");
361        }
362        let item_type = bytes[offset];
363        let codec = bytes[offset + 1];
364        let item_flags = bytes[offset + 2];
365        let reserved = bytes[offset + 3];
366        if item_flags != 0 {
367            bail!("sidecar item flags must be 0 in this version");
368        }
369        if reserved != 0 {
370            bail!("sidecar item reserved byte must be 0");
371        }
372        let raw_byte_length = read_u32be(bytes, offset + 4, "sidecar item raw length")?;
373        let stored_byte_length = read_u32be(bytes, offset + 8, "sidecar item stored length")?;
374        let name_len = read_u16be(bytes, offset + 12, "sidecar item name length")? as usize;
375        let mime_len = read_u16be(bytes, offset + 14, "sidecar item MIME length")? as usize;
376        offset += 16;
377        let item_end = offset
378            .checked_add(name_len)
379            .and_then(|value| value.checked_add(mime_len))
380            .and_then(|value| value.checked_add(stored_byte_length as usize))
381            .context("sidecar item length overflow")?;
382        if item_end > bytes.len() {
383            bail!("sidecar item payload is truncated");
384        }
385        let name = std::str::from_utf8(&bytes[offset..offset + name_len])
386            .context("sidecar item name is not valid UTF-8")?
387            .to_string();
388        offset += name_len;
389        let mime = std::str::from_utf8(&bytes[offset..offset + mime_len])
390            .context("sidecar item MIME type is not valid ASCII/UTF-8")?
391            .to_string();
392        offset += mime_len;
393        let stored = &bytes[offset..offset + stored_byte_length as usize];
394        offset += stored_byte_length as usize;
395
396        validate_sidecar_name(&name, "item name")?;
397        validate_sidecar_name(&mime, "MIME type")?;
398        validate_sidecar_item_payload(item_type, codec, raw_byte_length, stored, &mime)?;
399        items.push(SidecarItemValidation {
400            item_type,
401            item_type_name: sidecar_type_name(item_type),
402            codec,
403            codec_name: sidecar_codec_name(codec),
404            flags: item_flags,
405            raw_byte_length: (raw_byte_length != SIDECAR_RAW_LENGTH_ABSENT)
406                .then_some(raw_byte_length),
407            stored_byte_length,
408            name,
409            mime,
410        });
411    }
412    if offset != bytes.len() {
413        bail!("sidecar container has trailing bytes");
414    }
415    Ok(SidecarContainerValidation {
416        ok: true,
417        version,
418        flags,
419        item_count,
420        total_length,
421        items,
422    })
423}
424
425fn decode_sidecar_item_payload(
426    item_type: u8,
427    codec: u8,
428    raw_byte_length: u32,
429    stored: &[u8],
430) -> Result<Vec<u8>> {
431    let decoded = match codec {
432        SIDECAR_CODEC_RAW | SIDECAR_CODEC_AVIF => stored.to_vec(),
433        SIDECAR_CODEC_BROTLI => {
434            let mut reader = brotli::Decompressor::new(stored, 4096);
435            let mut out = Vec::new();
436            reader
437                .read_to_end(&mut out)
438                .context("failed to decompress Brotli sidecar item")?;
439            out
440        }
441        SIDECAR_CODEC_ZSTD => bail!("zstd sidecar item extraction is not implemented"),
442        _ => stored.to_vec(),
443    };
444    if raw_byte_length != SIDECAR_RAW_LENGTH_ABSENT && decoded.len() != raw_byte_length as usize {
445        bail!(
446            "decoded sidecar item length {} does not match declared raw length {}",
447            decoded.len(),
448            raw_byte_length
449        );
450    }
451    if item_type == SIDECAR_TYPE_UTF8_TEXT {
452        std::str::from_utf8(&decoded)
453            .context("decoded UTF-8 text sidecar item is not valid UTF-8")?;
454    }
455    if item_type == SIDECAR_TYPE_JSON {
456        serde_json::from_slice::<serde_json::Value>(&decoded)
457            .context("decoded JSON sidecar item is not valid JSON")?;
458    }
459    Ok(decoded)
460}
461
462pub fn decode_sidecar_container_items(bytes: &[u8]) -> Result<SidecarDecodedItems> {
463    let validation = validate_sidecar_container(bytes)?;
464    let item_count = read_u16be(bytes, 6, "sidecar item count")? as usize;
465    let mut offset = 12usize;
466    let mut items = Vec::with_capacity(item_count);
467    for _ in 0..item_count {
468        if offset + 16 > bytes.len() {
469            bail!("sidecar item header is truncated");
470        }
471        let item_type = bytes[offset];
472        let codec = bytes[offset + 1];
473        let flags = bytes[offset + 2];
474        let raw_byte_length = read_u32be(bytes, offset + 4, "sidecar item raw length")?;
475        let stored_byte_length = read_u32be(bytes, offset + 8, "sidecar item stored length")?;
476        let name_len = read_u16be(bytes, offset + 12, "sidecar item name length")? as usize;
477        let mime_len = read_u16be(bytes, offset + 14, "sidecar item MIME length")? as usize;
478        offset += 16;
479        let item_end = offset
480            .checked_add(name_len)
481            .and_then(|value| value.checked_add(mime_len))
482            .and_then(|value| value.checked_add(stored_byte_length as usize))
483            .context("sidecar item length overflow")?;
484        if item_end > bytes.len() {
485            bail!("sidecar item payload is truncated");
486        }
487        let name = std::str::from_utf8(&bytes[offset..offset + name_len])
488            .context("sidecar item name is not valid UTF-8")?
489            .to_string();
490        offset += name_len;
491        let mime = std::str::from_utf8(&bytes[offset..offset + mime_len])
492            .context("sidecar item MIME type is not valid ASCII/UTF-8")?
493            .to_string();
494        offset += mime_len;
495        let stored = &bytes[offset..offset + stored_byte_length as usize];
496        offset += stored_byte_length as usize;
497        let decoded = decode_sidecar_item_payload(item_type, codec, raw_byte_length, stored)?;
498        let text = if item_type == SIDECAR_TYPE_UTF8_TEXT {
499            Some(
500                std::str::from_utf8(&decoded)
501                    .context("decoded UTF-8 text sidecar item is not valid UTF-8")?
502                    .to_string(),
503            )
504        } else {
505            None
506        };
507        let json = if item_type == SIDECAR_TYPE_JSON {
508            Some(
509                serde_json::from_slice::<serde_json::Value>(&decoded)
510                    .context("decoded JSON sidecar item is not valid JSON")?,
511            )
512        } else {
513            None
514        };
515        items.push(SidecarDecodedItem {
516            item_type,
517            item_type_name: sidecar_type_name(item_type),
518            codec,
519            codec_name: sidecar_codec_name(codec),
520            flags,
521            raw_byte_length: (raw_byte_length != SIDECAR_RAW_LENGTH_ABSENT)
522                .then_some(raw_byte_length),
523            stored_byte_length,
524            decoded_byte_length: decoded.len(),
525            name,
526            mime,
527            stored_data_base64: general_purpose::STANDARD.encode(stored),
528            data_base64: general_purpose::STANDARD.encode(&decoded),
529            text,
530            json,
531        });
532    }
533    if offset != bytes.len() {
534        bail!("sidecar container has trailing bytes");
535    }
536    Ok(SidecarDecodedItems {
537        ok: true,
538        validation,
539        items,
540    })
541}
542
543pub fn parse_sidecar_carrier(raw: &str) -> Result<SidecarCarrier> {
544    match raw
545        .trim()
546        .to_ascii_lowercase()
547        .replace([' ', '-', '_'], "")
548        .as_str()
549    {
550        "label" => Ok(SidecarCarrier::Label),
551        "intergroove" | "intragroove" | "groove" => Ok(SidecarCarrier::Intergroove),
552        "leadindeadwax" | "leaddeadwax" | "leadin" | "leadout" | "deadwax" | "runout" => {
553            Ok(SidecarCarrier::LeadInDeadwax)
554        }
555        _ => bail!("unknown sidecar carrier: {raw}"),
556    }
557}
558
559pub fn sidecar_carrier_name(carrier: SidecarCarrier) -> &'static str {
560    match carrier {
561        SidecarCarrier::Label => "label",
562        SidecarCarrier::Intergroove => "intergroove",
563        SidecarCarrier::LeadInDeadwax => "leadInDeadwax",
564    }
565}
566
567pub fn normalize_sidecar_carriers(raw: Option<&[String]>) -> Result<Vec<SidecarCarrier>> {
568    let mut carriers = Vec::new();
569    if let Some(raw) = raw {
570        for value in raw {
571            let carrier = parse_sidecar_carrier(value)?;
572            if !carriers.contains(&carrier) {
573                carriers.push(carrier);
574            }
575        }
576    } else {
577        carriers.push(SidecarCarrier::Label);
578        carriers.push(SidecarCarrier::Intergroove);
579    }
580    if carriers.is_empty() {
581        bail!("sidecar carriers must not be empty");
582    }
583    Ok(carriers)
584}
585
586pub fn default_sidecar_carriers() -> Vec<SidecarCarrier> {
587    vec![SidecarCarrier::Label, SidecarCarrier::Intergroove]
588}
589
590pub fn normalize_sidecar_scheme(raw: Option<&str>) -> Result<String> {
591    let Some(raw) = raw else {
592        return Ok(SIDECAR_SCHEME_PAIRSIGN_SAFE_LUMA_V2.to_string());
593    };
594    let normalized = raw.trim().to_ascii_lowercase();
595    match normalized.as_str() {
596        SIDECAR_SCHEME_PAIRSIGN_SAFE_LUMA_V2 => Ok(normalized),
597        _ => bail!("unsupported sidecar carrier scheme {raw}"),
598    }
599}
600
601pub fn sidecar_pointer_scheme_id(scheme: &str) -> Result<u8> {
602    match normalize_sidecar_scheme(Some(scheme))?.as_str() {
603        SIDECAR_SCHEME_PAIRSIGN_SAFE_LUMA_V2 => Ok(SIDECAR_POINTER_SCHEME_PAIRSIGN_SAFE_LUMA_V2),
604        _ => unreachable!("scheme normalized"),
605    }
606}
607
608pub fn sidecar_pointer_scheme_name(scheme_id: u8) -> Result<String> {
609    match scheme_id {
610        SIDECAR_POINTER_SCHEME_PAIRSIGN_SAFE_LUMA_V2 => {
611            Ok(SIDECAR_SCHEME_PAIRSIGN_SAFE_LUMA_V2.to_string())
612        }
613        _ => bail!("unsupported sidecar pointer scheme id {scheme_id}"),
614    }
615}
616
617pub fn sidecar_pointer_carrier_flags(carriers: &[SidecarCarrier]) -> u8 {
618    let mut flags = 0u8;
619    if carriers.contains(&SidecarCarrier::Label) {
620        flags |= SIDECAR_POINTER_CARRIER_LABEL;
621    }
622    if carriers.contains(&SidecarCarrier::Intergroove) {
623        flags |= SIDECAR_POINTER_CARRIER_INTERGROOVE;
624    }
625    if carriers.contains(&SidecarCarrier::LeadInDeadwax) {
626        flags |= SIDECAR_POINTER_CARRIER_LEAD_IN_DEADWAX;
627    }
628    flags
629}
630
631pub fn sidecar_pointer_carriers(flags: u8) -> Result<Vec<SidecarCarrier>> {
632    if flags
633        & !(SIDECAR_POINTER_CARRIER_LABEL
634            | SIDECAR_POINTER_CARRIER_INTERGROOVE
635            | SIDECAR_POINTER_CARRIER_LEAD_IN_DEADWAX)
636        != 0
637    {
638        bail!("sidecar pointer has unsupported carrier flags {flags:#04x}");
639    }
640    let mut carriers = Vec::new();
641    if flags & SIDECAR_POINTER_CARRIER_LABEL != 0 {
642        carriers.push(SidecarCarrier::Label);
643    }
644    if flags & SIDECAR_POINTER_CARRIER_INTERGROOVE != 0 {
645        carriers.push(SidecarCarrier::Intergroove);
646    }
647    if flags & SIDECAR_POINTER_CARRIER_LEAD_IN_DEADWAX != 0 {
648        carriers.push(SidecarCarrier::LeadInDeadwax);
649    }
650    if carriers.is_empty() {
651        bail!("sidecar pointer has no carriers");
652    }
653    Ok(carriers)
654}
655
656
657pub fn decode_sidecar_header_pointer(payload: &[u8]) -> Result<SidecarHeaderPointer> {
658    if payload.len() != SIDECAR_POINTER_LENGTH {
659        bail!("sidecar pointer has invalid length {}", payload.len());
660    }
661    if &payload[..4] != SIDECAR_MAGIC {
662        bail!("sidecar pointer magic is unsupported");
663    }
664    let version = payload[4];
665    if version != SIDECAR_POINTER_VERSION {
666        bail!("unsupported sidecar pointer version {version}");
667    }
668    let scheme = sidecar_pointer_scheme_name(payload[5])?;
669    let carriers = sidecar_pointer_carriers(payload[6])?;
670    if payload[7] != 0 {
671        bail!("sidecar pointer reserved byte must be 0");
672    }
673    let seed = u32::from_be_bytes(payload[8..12].try_into().expect("slice length"));
674    if seed == 0 {
675        bail!("sidecar seed must be nonzero");
676    }
677    let length = u32::from_be_bytes(payload[12..16].try_into().expect("slice length")) as usize;
678    if length == 0 {
679        bail!("sidecar length must be nonzero");
680    }
681    let mut sha256_bytes = [0u8; 32];
682    sha256_bytes.copy_from_slice(&payload[16..48]);
683    let sha256 = general_purpose::URL_SAFE_NO_PAD.encode(sha256_bytes);
684    Ok(SidecarHeaderPointer {
685        scheme,
686        carriers,
687        seed,
688        length,
689        sha256,
690        sha256_bytes,
691    })
692}
693
694pub fn sidecar_header_pointer_json(pointer: &SidecarHeaderPointer) -> serde_json::Value {
695    serde_json::json!({
696        "v": SIDECAR_POINTER_VERSION,
697        "c": "BTS1",
698        "s": pointer.scheme.as_str(),
699        "r": pointer.carriers.iter().map(|carrier| sidecar_carrier_name(*carrier)).collect::<Vec<_>>(),
700        "n": pointer.seed,
701        "l": pointer.length,
702        "h": pointer.sha256.as_str(),
703    })
704}
705
706
707pub fn mulberry32_next(state: &mut u32) -> u32 {
708    *state = state.wrapping_add(0x6d2b_79f5);
709    let mut t = *state;
710    t = (t ^ (t >> 15)).wrapping_mul(t | 1);
711    t ^= t.wrapping_add((t ^ (t >> 7)).wrapping_mul(t | 61));
712    t ^ (t >> 14)
713}
714
715pub fn shuffle_pairs_mulberry32(pairs: &mut [(usize, usize)], seed: u32) {
716    let mut state = seed;
717    for i in (1..pairs.len()).rev() {
718        let j = (mulberry32_next(&mut state) as usize) % (i + 1);
719        pairs.swap(i, j);
720    }
721}
722
723pub use bytes2rgb::luma_rec709 as sidecar_luma_rec709;
724
725pub fn metadata_dither(pixel_index: usize, sequence_index: usize, salt: usize) -> u8 {
726    let mut value = pixel_index as u64;
727    value ^= (sequence_index as u64).wrapping_mul(0x9e37_79b9_7f4a_7c15);
728    value ^= (salt as u64).wrapping_mul(0xbf58_476d_1ce4_e5b9);
729    value ^= value >> 30;
730    value = value.wrapping_mul(0xbf58_476d_1ce4_e5b9);
731    value ^= value >> 27;
732    value = value.wrapping_mul(0x94d0_49bb_1331_11eb);
733    value ^= value >> 31;
734    (value & 0xff) as u8
735}
736
737
738pub fn sidecar_capacity_bytes_for_scheme(scheme: &str, carrier_pairs: usize) -> Result<usize> {
739    match normalize_sidecar_scheme(Some(scheme))?.as_str() {
740        SIDECAR_SCHEME_PAIRSIGN_SAFE_LUMA_V2 => Ok(carrier_pairs / 4),
741        _ => unreachable!("scheme normalized"),
742    }
743}
744
745pub fn sidecar_pair_bit_width_for_scheme(
746    scheme: &str,
747    _rgba: &[u8],
748    _first_pixel: usize,
749    _second_pixel: usize,
750) -> Result<usize> {
751    match normalize_sidecar_scheme(Some(scheme))?.as_str() {
752        SIDECAR_SCHEME_PAIRSIGN_SAFE_LUMA_V2 => Ok(2),
753        _ => unreachable!("scheme normalized"),
754    }
755}
756
757pub fn sidecar_bit_capacity_for_pairs(
758    scheme: &str,
759    pairs: &[(usize, usize)],
760    rgba: &[u8],
761) -> Result<usize> {
762    let mut bits = 0usize;
763    for &(first, second) in pairs {
764        bits = bits
765            .checked_add(sidecar_pair_bit_width_for_scheme(
766                scheme, rgba, first, second,
767            )?)
768            .context("sidecar pair bit capacity overflow")?;
769    }
770    Ok(bits)
771}
772
773pub fn sidecar_capacity_for_pairs(
774    scheme: &str,
775    carriers: &[SidecarCarrier],
776    pairs: &[(usize, usize)],
777    rgba: &[u8],
778) -> Result<SidecarCapacity> {
779    let bit_capacity = sidecar_bit_capacity_for_pairs(scheme, pairs, rgba)?;
780    let two_bit_pairs = pairs
781        .iter()
782        .filter_map(|&(first, second)| {
783            sidecar_pair_bit_width_for_scheme(scheme, rgba, first, second).ok()
784        })
785        .filter(|width| *width == 2)
786        .count();
787    Ok(SidecarCapacity {
788        scheme: normalize_sidecar_scheme(Some(scheme))?,
789        carriers: carriers
790            .iter()
791            .map(|carrier| sidecar_carrier_name(*carrier).to_string())
792            .collect(),
793        carrier_pixels: pairs.len() * 2,
794        carrier_pairs: pairs.len(),
795        capacity_bits: bit_capacity,
796        capacity_bytes: bit_capacity / 8,
797        bits_per_pair: if pairs.is_empty() {
798            0.0
799        } else {
800            bit_capacity as f64 / pairs.len() as f64
801        },
802        two_bit_pairs,
803    })
804}
805
806pub fn decode_pairsign_sidecar_bytes_from_pairs(
807    rgba: &[u8],
808    pairs: &[(usize, usize)],
809    scheme: &str,
810    byte_length: usize,
811) -> Result<Vec<u8>> {
812    let bit_capacity = sidecar_bit_capacity_for_pairs(scheme, pairs, rgba)?;
813    if byte_length.saturating_mul(8) > bit_capacity {
814        bail!(
815            "sidecar descriptor length {} exceeds pair-sign carrier capacity {}",
816            byte_length,
817            bit_capacity / 8
818        );
819    }
820    let target_bits = byte_length
821        .checked_mul(8)
822        .context("sidecar decode bit length overflow")?;
823    let mut out = vec![0u8; byte_length];
824    let mut bit_index = 0usize;
825
826    fn push_sidecar_decoded_bit(out: &mut [u8], bit_index: usize, bit: u8) {
827        if bit != 0 {
828            let byte_index = bit_index / 8;
829            let shift = 7 - (bit_index % 8);
830            out[byte_index] |= 1 << shift;
831        }
832    }
833
834    for &(first, second) in pairs {
835        if bit_index >= target_bits {
836            break;
837        }
838        let first_luma = sidecar_luma_rec709(rgba, first);
839        let second_luma = sidecar_luma_rec709(rgba, second);
840        let sign_bit = if first_luma > second_luma { 1u8 } else { 0u8 };
841        push_sidecar_decoded_bit(&mut out, bit_index, sign_bit);
842        bit_index += 1;
843        if bit_index < target_bits
844            && sidecar_pair_bit_width_for_scheme(scheme, rgba, first, second)? == 2
845        {
846            let magnitude_bit =
847                if (first_luma - second_luma).abs() >= SIDECAR_PAIR_MAGNITUDE_THRESHOLD {
848                    1u8
849                } else {
850                    0u8
851                };
852            push_sidecar_decoded_bit(&mut out, bit_index, magnitude_bit);
853            bit_index += 1;
854        }
855    }
856    if bit_index < target_bits {
857        bail!("sidecar carrier did not provide enough bits to decode descriptor length");
858    }
859    Ok(out)
860}
861
862
863pub fn decode_sidecar_from_pairs(
864    rgba: &[u8],
865    pairs: &[(usize, usize)],
866    scheme: &str,
867    byte_length: usize,
868) -> Result<(Vec<u8>, SidecarDecodeResult)> {
869    let bts1 = decode_pairsign_sidecar_bytes_from_pairs(rgba, pairs, scheme, byte_length)?;
870    let validation = validate_sidecar_container(&bts1)?;
871    let sha256 = sha256_base64url(&bts1);
872    let capacity = sidecar_capacity_bytes_for_scheme(scheme, pairs.len())?;
873    let descriptor = serde_json::json!({
874        "container": "BTS1",
875        "scheme": normalize_sidecar_scheme(Some(scheme))?,
876        "length": byte_length,
877    });
878    let result = SidecarDecodeResult {
879        ok: true,
880        descriptor,
881        validation,
882        bts1_byte_length: bts1.len(),
883        sha256,
884        carrier_pixels: pairs.len() * 2,
885        carrier_pairs: pairs.len(),
886        capacity_bytes: capacity,
887    };
888    Ok((bts1, result))
889}