use std::io::{Write, Seek, SeekFrom, Read};
use crate::common::error::{MutagenError, Result};
use crate::vorbis::VorbisComment;
#[derive(Debug, Clone, Copy, PartialEq)]
#[repr(u8)]
pub enum BlockType {
StreamInfo = 0,
Padding = 1,
Application = 2,
SeekTable = 3,
VorbisComment = 4,
CueSheet = 5,
Picture = 6,
Unknown(u8),
}
impl BlockType {
pub fn from_byte(b: u8) -> Self {
match b {
0 => BlockType::StreamInfo,
1 => BlockType::Padding,
2 => BlockType::Application,
3 => BlockType::SeekTable,
4 => BlockType::VorbisComment,
5 => BlockType::CueSheet,
6 => BlockType::Picture,
n => BlockType::Unknown(n),
}
}
pub fn to_byte(&self) -> u8 {
match self {
BlockType::StreamInfo => 0,
BlockType::Padding => 1,
BlockType::Application => 2,
BlockType::SeekTable => 3,
BlockType::VorbisComment => 4,
BlockType::CueSheet => 5,
BlockType::Picture => 6,
BlockType::Unknown(n) => *n,
}
}
}
#[derive(Debug, Clone)]
pub struct MetadataBlock {
pub block_type: BlockType,
pub is_last: bool,
pub data: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct StreamInfo {
pub min_block_size: u16,
pub max_block_size: u16,
pub min_frame_size: u32,
pub max_frame_size: u32,
pub sample_rate: u32,
pub channels: u8,
pub bits_per_sample: u8,
pub total_samples: u64,
pub md5: [u8; 16],
pub length: f64,
}
impl StreamInfo {
pub fn parse(data: &[u8]) -> Result<Self> {
if data.len() < 34 {
return Err(MutagenError::FLAC("StreamInfo block too short".into()));
}
let min_block_size = u16::from_be_bytes([data[0], data[1]]);
let max_block_size = u16::from_be_bytes([data[2], data[3]]);
let min_frame_size = u32::from_be_bytes([0, data[4], data[5], data[6]]);
let max_frame_size = u32::from_be_bytes([0, data[7], data[8], data[9]]);
let sr_hi = u32::from_be_bytes([0, data[10], data[11], data[12]]);
let sample_rate = (sr_hi >> 4) & 0xFFFFF;
let channels = ((data[12] >> 1) & 0x07) + 1 ;
let bps_hi = (data[12] & 0x01) << 4;
let bps_lo = (data[13] >> 4) & 0x0F;
let bits_per_sample = (bps_hi | bps_lo) + 1;
let total_samples_hi = ((data[13] & 0x0F) as u64) << 32;
let total_samples_lo = u32::from_be_bytes([data[14], data[15], data[16], data[17]]) as u64;
let total_samples = total_samples_hi | total_samples_lo;
let mut md5 = [0u8; 16];
md5.copy_from_slice(&data[18..34]);
let length = if sample_rate > 0 {
total_samples as f64 / sample_rate as f64
} else {
0.0
};
Ok(StreamInfo {
min_block_size,
max_block_size,
min_frame_size,
max_frame_size,
sample_rate,
channels,
bits_per_sample,
total_samples,
md5,
length,
})
}
}
#[derive(Debug, Clone)]
pub struct FLACPicture {
pub pic_type: u32,
pub mime: String,
pub desc: String,
pub width: u32,
pub height: u32,
pub depth: u32,
pub colors: u32,
pub data: Vec<u8>,
}
impl FLACPicture {
pub fn parse(block_data: &[u8]) -> Result<Self> {
if block_data.len() < 32 {
return Err(MutagenError::FLAC("Picture block too short".into()));
}
let mut pos = 0;
let pic_type = u32::from_be_bytes([
block_data[pos], block_data[pos + 1], block_data[pos + 2], block_data[pos + 3],
]);
pos += 4;
let mime_len = u32::from_be_bytes([
block_data[pos], block_data[pos + 1], block_data[pos + 2], block_data[pos + 3],
]) as usize;
pos += 4;
if pos + mime_len > block_data.len() {
return Err(MutagenError::FLAC("Picture MIME extends past data".into()));
}
let mime = String::from_utf8_lossy(&block_data[pos..pos + mime_len]).into_owned();
pos += mime_len;
if pos + 4 > block_data.len() {
return Err(MutagenError::FLAC("Picture block too short for desc".into()));
}
let desc_len = u32::from_be_bytes([
block_data[pos], block_data[pos + 1], block_data[pos + 2], block_data[pos + 3],
]) as usize;
pos += 4;
if pos + desc_len > block_data.len() {
return Err(MutagenError::FLAC("Picture desc extends past data".into()));
}
let desc = String::from_utf8_lossy(&block_data[pos..pos + desc_len]).into_owned();
pos += desc_len;
if pos + 20 > block_data.len() {
return Err(MutagenError::FLAC("Picture block too short for dimensions".into()));
}
let width = u32::from_be_bytes([
block_data[pos], block_data[pos + 1], block_data[pos + 2], block_data[pos + 3],
]);
pos += 4;
let height = u32::from_be_bytes([
block_data[pos], block_data[pos + 1], block_data[pos + 2], block_data[pos + 3],
]);
pos += 4;
let depth = u32::from_be_bytes([
block_data[pos], block_data[pos + 1], block_data[pos + 2], block_data[pos + 3],
]);
pos += 4;
let colors = u32::from_be_bytes([
block_data[pos], block_data[pos + 1], block_data[pos + 2], block_data[pos + 3],
]);
pos += 4;
let data_len = u32::from_be_bytes([
block_data[pos], block_data[pos + 1], block_data[pos + 2], block_data[pos + 3],
]) as usize;
pos += 4;
let data = if pos + data_len <= block_data.len() {
block_data[pos..pos + data_len].to_vec()
} else {
block_data[pos..].to_vec()
};
Ok(FLACPicture {
pic_type,
mime,
desc,
width,
height,
depth,
colors,
data,
})
}
pub fn render(&self) -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(&self.pic_type.to_be_bytes());
let mime_bytes = self.mime.as_bytes();
data.extend_from_slice(&(mime_bytes.len() as u32).to_be_bytes());
data.extend_from_slice(mime_bytes);
let desc_bytes = self.desc.as_bytes();
data.extend_from_slice(&(desc_bytes.len() as u32).to_be_bytes());
data.extend_from_slice(desc_bytes);
data.extend_from_slice(&self.width.to_be_bytes());
data.extend_from_slice(&self.height.to_be_bytes());
data.extend_from_slice(&self.depth.to_be_bytes());
data.extend_from_slice(&self.colors.to_be_bytes());
data.extend_from_slice(&(self.data.len() as u32).to_be_bytes());
data.extend_from_slice(&self.data);
data
}
}
#[derive(Debug, Clone)]
pub struct LazyPicture {
pub block_offset: usize,
pub block_size: usize,
}
#[derive(Debug, Clone)]
pub struct BlockDesc {
pub block_type: BlockType,
pub is_last: bool,
pub data_offset: usize,
pub data_size: usize,
}
pub fn compute_vc_data_size(data: &[u8]) -> Option<usize> {
if data.len() < 8 { return None; }
let vendor_len = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
let mut pos = 4 + vendor_len;
if pos + 4 > data.len() { return None; }
let count = u32::from_le_bytes([data[pos], data[pos+1], data[pos+2], data[pos+3]]) as usize;
pos += 4;
for _ in 0..count {
if pos + 4 > data.len() { return None; }
let comment_len = u32::from_le_bytes([data[pos], data[pos+1], data[pos+2], data[pos+3]]) as usize;
pos += 4 + comment_len;
}
Some(pos)
}
#[derive(Debug)]
pub struct FLACFile {
pub info: StreamInfo,
pub tags: Option<VorbisComment>,
pub vc_raw: Option<Vec<u8>>, pub pictures: Vec<FLACPicture>,
pub lazy_pictures: Vec<LazyPicture>,
pub block_descs: Vec<BlockDesc>, pub path: String,
pub metadata_length: usize,
pub flac_offset: usize,
}
impl FLACFile {
pub fn open(path: &str) -> Result<Self> {
let data = std::fs::read(path)?;
Self::parse(&data, path)
}
pub fn parse(data: &[u8], path: &str) -> Result<Self> {
if data.len() < 4 || &data[0..4] != b"fLaC" {
let offset = if data.len() >= 3 && &data[0..3] == b"ID3" {
if data.len() >= 10 {
let size = crate::id3::header::BitPaddedInt::syncsafe(&data[6..10]) as usize;
10 + size
} else {
return Err(MutagenError::FLACNoHeader);
}
} else {
return Err(MutagenError::FLACNoHeader);
};
if offset + 4 > data.len() || &data[offset..offset + 4] != b"fLaC" {
return Err(MutagenError::FLACNoHeader);
}
return Self::parse_from_offset(data, offset, path);
}
Self::parse_from_offset(data, 0, path)
}
fn parse_from_offset(data: &[u8], flac_offset: usize, path: &str) -> Result<Self> {
let mut pos = flac_offset + 4; let mut block_descs = Vec::new();
let mut stream_info = None;
let mut vc_raw = None;
let mut lazy_pictures = Vec::new();
loop {
if pos + 4 > data.len() {
break;
}
let header_byte = data[pos];
let is_last = header_byte & 0x80 != 0;
let block_type = BlockType::from_byte(header_byte & 0x7F);
let block_size = u32::from_be_bytes([0, data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
pos += 4;
if pos + block_size > data.len() {
break;
}
block_descs.push(BlockDesc {
block_type,
is_last,
data_offset: pos,
data_size: block_size,
});
match block_type {
BlockType::StreamInfo => {
stream_info = Some(StreamInfo::parse(&data[pos..pos + block_size])?);
}
BlockType::VorbisComment => {
let avail = &data[pos..];
let vc_size = compute_vc_data_size(avail).unwrap_or(block_size);
let end = pos + vc_size.min(data.len() - pos);
vc_raw = Some(data[pos..end].to_vec());
}
BlockType::Picture => {
lazy_pictures.push(LazyPicture {
block_offset: pos,
block_size,
});
}
_ => {}
}
pos += block_size;
if is_last {
break;
}
}
let info = stream_info.ok_or_else(|| MutagenError::FLAC("No StreamInfo block found".into()))?;
Ok(FLACFile {
info,
tags: None,
vc_raw,
pictures: Vec::new(),
lazy_pictures,
block_descs,
path: path.to_string(),
metadata_length: pos - flac_offset,
flac_offset,
})
}
pub fn ensure_tags(&mut self) {
if self.tags.is_none() {
if let Some(ref raw) = self.vc_raw {
self.tags = VorbisComment::parse(raw, false).ok();
}
}
}
pub fn get_tags(&mut self) -> Option<&VorbisComment> {
self.ensure_tags();
self.tags.as_ref()
}
pub fn save(&self) -> Result<()> {
let mut file = std::fs::OpenOptions::new().read(true).write(true).open(&self.path)?;
let mut existing = Vec::new();
file.read_to_end(&mut existing)?;
let flac_offset = if existing.len() >= 4 && &existing[0..4] == b"fLaC" {
0
} else if existing.len() >= 3 && &existing[0..3] == b"ID3" {
if existing.len() >= 10 {
let size = crate::id3::header::BitPaddedInt::syncsafe(&existing[6..10]) as usize;
10 + size
} else {
return Err(MutagenError::FLAC("Cannot find fLaC header".into()));
}
} else {
return Err(MutagenError::FLAC("Cannot find fLaC header".into()));
};
let mut new_metadata = Vec::new();
new_metadata.extend_from_slice(b"fLaC");
let mut blocks_to_write: Vec<(BlockType, Vec<u8>)> = Vec::new();
for bd in &self.block_descs {
if bd.block_type == BlockType::StreamInfo {
if bd.data_offset + bd.data_size <= existing.len() {
blocks_to_write.push((BlockType::StreamInfo, existing[bd.data_offset..bd.data_offset + bd.data_size].to_vec()));
}
break;
}
}
if let Some(ref vc) = self.tags {
blocks_to_write.push((BlockType::VorbisComment, vc.render(false)));
} else if let Some(ref raw) = self.vc_raw {
blocks_to_write.push((BlockType::VorbisComment, raw.clone()));
}
for pic in &self.pictures {
blocks_to_write.push((BlockType::Picture, pic.render()));
}
for lp in &self.lazy_pictures {
if lp.block_offset + lp.block_size <= existing.len() {
blocks_to_write.push((BlockType::Picture, existing[lp.block_offset..lp.block_offset + lp.block_size].to_vec()));
}
}
for bd in &self.block_descs {
match bd.block_type {
BlockType::StreamInfo | BlockType::VorbisComment | BlockType::Picture | BlockType::Padding => {}
_ => {
if bd.data_offset + bd.data_size <= existing.len() {
blocks_to_write.push((bd.block_type, existing[bd.data_offset..bd.data_offset + bd.data_size].to_vec()));
}
}
}
}
blocks_to_write.push((BlockType::Padding, vec![0u8; 1024]));
for (i, (block_type, block_data)) in blocks_to_write.iter().enumerate() {
let is_last = i == blocks_to_write.len() - 1;
let header_byte = if is_last {
block_type.to_byte() | 0x80
} else {
block_type.to_byte()
};
new_metadata.push(header_byte);
let size = block_data.len() as u32;
new_metadata.push((size >> 16) as u8);
new_metadata.push((size >> 8) as u8);
new_metadata.push(size as u8);
new_metadata.extend_from_slice(block_data);
}
let audio_start = flac_offset + self.metadata_length;
let audio_data = &existing[audio_start..];
file.seek(SeekFrom::Start(flac_offset as u64))?;
file.set_len(flac_offset as u64)?;
file.write_all(&new_metadata)?;
file.write_all(audio_data)?;
file.flush()?;
Ok(())
}
pub fn score(path: &str, data: &[u8]) -> u32 {
let mut score = 0u32;
let ext = path.rsplit('.').next().unwrap_or("");
if ext.eq_ignore_ascii_case("flac") {
score += 2;
}
if data.len() >= 4 && &data[0..4] == b"fLaC" {
score += 3;
}
score
}
}