use crate::scarletbook::consts;
use crate::scarletbook::types;
use anyhow::Result;
use byteorder::{BigEndian, ReadBytesExt};
use std::io::{Cursor, Read};
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct MasterToc {
pub id: [u8; 8], pub version: types::Version, pub reserved01: [u8; 6],
pub album_set_size: u16, pub album_sequence_number: u16, pub reserved02: [u8; 4],
pub album_catalog_number: [u8; 16], pub album_genre: [types::GenreTable; 4], pub reserved03: [u8; 8],
pub area_1_toc_1_start: u32, pub area_1_toc_2_start: u32, pub area_2_toc_1_start: u32, pub area_2_toc_2_start: u32, pub disc_flags: u8, pub reserved04: [u8; 3],
pub area_1_toc_size: u16, pub area_2_toc_size: u16, pub disc_catalog_number: [u8; 16], pub disc_genre: [types::GenreTable; 4], pub disc_date_year: u16, pub disc_date_month: u8,
pub disc_date_day: u8,
pub reserved05: [u8; 4],
pub text_area_count: u8, pub reserved06: [u8; 7],
pub locales: [types::LocaleTable; 8], }
impl MasterToc {
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
let mut cursor = Cursor::new(bytes);
Self::read_from(&mut cursor)
}
pub fn read_from<R: Read>(reader: &mut R) -> Result<Self> {
let mut id = [0u8; 8];
reader.read_exact(&mut id)?;
if &id != consts::MASTER_TOC_SIGNATURE {
anyhow::bail!(
"Master TOC signature mismatch: expected {:?}, got {:?} — not a Scarlet Book disc",
std::str::from_utf8(consts::MASTER_TOC_SIGNATURE).unwrap_or("?"),
String::from_utf8_lossy(&id),
);
}
let version = types::Version::parse(reader)?;
let mut reserved01 = [0u8; 6];
reader.read_exact(&mut reserved01)?;
let album_set_size = reader.read_u16::<BigEndian>()?;
let album_sequence_number = reader.read_u16::<BigEndian>()?;
let mut reserved02 = [0u8; 4];
reader.read_exact(&mut reserved02)?;
let mut album_catalog_number = [0u8; 16];
reader.read_exact(&mut album_catalog_number)?;
let album_genre = [
types::GenreTable::parse(reader)?,
types::GenreTable::parse(reader)?,
types::GenreTable::parse(reader)?,
types::GenreTable::parse(reader)?,
];
let mut reserved03 = [0u8; 8];
reader.read_exact(&mut reserved03)?;
let area_1_toc_1_start = reader.read_u32::<BigEndian>()?;
let area_1_toc_2_start = reader.read_u32::<BigEndian>()?;
let area_2_toc_1_start = reader.read_u32::<BigEndian>()?;
let area_2_toc_2_start = reader.read_u32::<BigEndian>()?;
let disc_flags = reader.read_u8()?;
let mut reserved04 = [0u8; 3];
reader.read_exact(&mut reserved04)?;
let area_1_toc_size = reader.read_u16::<BigEndian>()?;
let area_2_toc_size = reader.read_u16::<BigEndian>()?;
let mut disc_catalog_number = [0u8; 16];
reader.read_exact(&mut disc_catalog_number)?;
let disc_genre = [
types::GenreTable::parse(reader)?,
types::GenreTable::parse(reader)?,
types::GenreTable::parse(reader)?,
types::GenreTable::parse(reader)?,
];
let disc_date_year = reader.read_u16::<BigEndian>()?;
let disc_date_month = reader.read_u8()?;
let disc_date_day = reader.read_u8()?;
let mut reserved05 = [0u8; 4];
reader.read_exact(&mut reserved05)?;
let text_area_count = reader.read_u8()?;
let mut reserved06 = [0u8; 7];
reader.read_exact(&mut reserved06)?;
let locales = [
types::LocaleTable::parse(reader)?,
types::LocaleTable::parse(reader)?,
types::LocaleTable::parse(reader)?,
types::LocaleTable::parse(reader)?,
types::LocaleTable::parse(reader)?,
types::LocaleTable::parse(reader)?,
types::LocaleTable::parse(reader)?,
types::LocaleTable::parse(reader)?,
];
Ok(Self {
id,
version,
reserved01,
album_set_size,
album_sequence_number,
reserved02,
album_catalog_number,
album_genre,
reserved03,
area_1_toc_1_start,
area_1_toc_2_start,
area_2_toc_1_start,
area_2_toc_2_start,
disc_flags,
reserved04,
area_1_toc_size,
area_2_toc_size,
disc_catalog_number,
disc_genre,
disc_date_year,
disc_date_month,
disc_date_day,
reserved05,
text_area_count,
reserved06,
locales,
})
}
pub fn disc_catalog(&self) -> String {
String::from_utf8_lossy(&self.disc_catalog_number)
.trim_end_matches('\0')
.trim()
.to_string()
}
pub fn is_hybrid(&self) -> bool {
(self.disc_flags & 0x80) != 0
}
pub fn has_two_channel(&self) -> bool {
self.area_1_toc_1_start != 0
}
pub fn has_multi_channel(&self) -> bool {
self.area_2_toc_1_start != 0
}
pub fn disc_category(&self) -> Option<&'static str> {
self.disc_genre
.iter()
.find(|g| g.category == 1)
.and_then(|g| g.category_name())
}
pub fn disc_genre(&self) -> Option<&'static str> {
self.disc_genre
.iter()
.find(|g| g.category == 1)
.and_then(|g| g.genre_name())
}
}
#[derive(Debug, Clone, Default)]
pub struct MasterText {
pub album_title: Option<String>,
pub album_artist: Option<String>,
pub album_publisher: Option<String>,
pub album_copyright: Option<String>,
pub disc_title: Option<String>,
pub disc_artist: Option<String>,
pub disc_publisher: Option<String>,
pub disc_copyright: Option<String>,
}
impl MasterText {
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
if bytes.len() < 2048 {
anyhow::bail!("Master text data too short");
}
let mut cursor = Cursor::new(bytes);
let mut id = [0u8; 8];
cursor.read_exact(&mut id)?;
if &id != consts::MASTER_TEXT_SIGNATURE {
anyhow::bail!("Invalid master text signature");
}
cursor.set_position(16);
let album_title_pos = cursor.read_u16::<BigEndian>()?;
let album_artist_pos = cursor.read_u16::<BigEndian>()?;
let album_publisher_pos = cursor.read_u16::<BigEndian>()?;
let album_copyright_pos = cursor.read_u16::<BigEndian>()?;
let _album_title_phonetic_pos = cursor.read_u16::<BigEndian>()?;
let _album_artist_phonetic_pos = cursor.read_u16::<BigEndian>()?;
let _album_publisher_phonetic_pos = cursor.read_u16::<BigEndian>()?;
let _album_copyright_phonetic_pos = cursor.read_u16::<BigEndian>()?;
let disc_title_pos = cursor.read_u16::<BigEndian>()?;
let disc_artist_pos = cursor.read_u16::<BigEndian>()?;
let disc_publisher_pos = cursor.read_u16::<BigEndian>()?;
let disc_copyright_pos = cursor.read_u16::<BigEndian>()?;
let extract_string = |pos: u16| -> Option<String> {
if pos == 0 || pos as usize >= bytes.len() {
return None;
}
let start = pos as usize;
let end = bytes[start..]
.iter()
.position(|&b| b == 0)
.map(|i| start + i)
.unwrap_or(bytes.len());
let s = String::from_utf8_lossy(&bytes[start..end])
.trim()
.to_string();
if s.is_empty() { None } else { Some(s) }
};
Ok(MasterText {
album_title: extract_string(album_title_pos),
album_artist: extract_string(album_artist_pos),
album_publisher: extract_string(album_publisher_pos),
album_copyright: extract_string(album_copyright_pos),
disc_title: extract_string(disc_title_pos),
disc_artist: extract_string(disc_artist_pos),
disc_publisher: extract_string(disc_publisher_pos),
disc_copyright: extract_string(disc_copyright_pos),
})
}
}