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}