use crate::constants;
use crate::id3::frames::{COMM, TCON, TextFrame};
use crate::id3::specs::TextEncoding;
use crate::{AudexError, Result};
use std::collections::HashMap;
type ID3v1FindResult = (
Option<HashMap<String, Box<dyn crate::id3::frames::Frame>>>,
i64,
);
#[derive(Debug, Clone)]
pub struct ID3v1Tag {
pub title: String,
pub artist: String,
pub album: String,
pub year: String,
pub comment: String,
pub track: Option<u8>,
pub genre: u8,
}
impl ID3v1Tag {
pub fn from_bytes(data: &[u8]) -> Result<Self> {
if data.len() != 128 {
return Err(AudexError::InvalidData(
"ID3v1 tag must be exactly 128 bytes".to_string(),
));
}
if &data[0..3] != b"TAG" {
return Err(AudexError::InvalidData(
"Invalid ID3v1 tag header".to_string(),
));
}
let title = extract_field(&data[3..33]);
let artist = extract_field(&data[33..63]);
let album = extract_field(&data[63..93]);
let year = extract_field(&data[93..97]);
let (comment, track) = if data[125] == 0 && data[126] != 0 {
(extract_field(&data[97..125]), Some(data[126]))
} else {
(extract_field(&data[97..127]), None)
};
let genre = data[127];
Ok(Self {
title,
artist,
album,
year,
comment,
track,
genre,
})
}
pub fn to_bytes(&self) -> [u8; 128] {
let mut data = [0u8; 128];
data[0..3].copy_from_slice(b"TAG");
write_field(&mut data[3..33], &self.title);
write_field(&mut data[33..63], &self.artist);
write_field(&mut data[63..93], &self.album);
write_field(&mut data[93..97], &self.year);
if let Some(track) = self.track {
write_field(&mut data[97..125], &self.comment);
data[125] = 0; data[126] = track;
} else {
write_field(&mut data[97..127], &self.comment);
}
data[127] = self.genre;
data
}
pub fn genre_name(&self) -> Option<&'static str> {
constants::get_genre(self.genre)
}
pub fn set_genre_name(&mut self, name: &str) -> bool {
if let Some(id) = constants::find_genre_id(name) {
self.genre = id;
true
} else {
false
}
}
pub fn is_v11(&self) -> bool {
self.track.is_some()
}
pub fn new() -> Self {
Self {
title: String::new(),
artist: String::new(),
album: String::new(),
year: String::new(),
comment: String::new(),
track: None,
genre: 255, }
}
pub fn with_track(track: u8) -> Self {
Self {
title: String::new(),
artist: String::new(),
album: String::new(),
year: String::new(),
comment: String::new(),
track: Some(track),
genre: 255, }
}
pub fn is_empty(&self) -> bool {
self.title.is_empty()
&& self.artist.is_empty()
&& self.album.is_empty()
&& self.year.is_empty()
&& self.comment.is_empty()
&& self.track.is_none()
}
}
impl Default for ID3v1Tag {
fn default() -> Self {
Self::new()
}
}
fn extract_field(data: &[u8]) -> String {
let end = data.iter().position(|&b| b == 0).unwrap_or(data.len());
let latin1_bytes = &data[..end];
let mut result = String::new();
for &byte in latin1_bytes {
result.push(byte as char);
}
result.trim().to_string()
}
fn encode_latin1(text: &str) -> Vec<u8> {
text.chars()
.map(|c| if c as u32 <= 0xFF { c as u8 } else { b'?' })
.collect()
}
fn write_field(field: &mut [u8], text: &str) {
let bytes = encode_latin1(text);
let len = bytes.len().min(field.len());
field[..len].copy_from_slice(&bytes[..len]);
}
pub fn find_id3v1_tag(data: &[u8]) -> Option<ID3v1Tag> {
if data.len() < 128 {
return None;
}
let tag_data = &data[data.len() - 128..];
if !validate_tag_heuristics(tag_data) {
return None;
}
ID3v1Tag::from_bytes(tag_data).ok()
}
pub fn remove_id3v1_tag(data: &mut Vec<u8>) -> bool {
if data.len() >= 128 && &data[data.len() - 128..data.len() - 125] == b"TAG" {
data.truncate(data.len() - 128);
true
} else {
false
}
}
pub fn write_id3v1_tag(data: &mut Vec<u8>, tag: &ID3v1Tag) {
remove_id3v1_tag(data);
data.extend_from_slice(&tag.to_bytes());
}
pub fn find_id3v1(
data: &[u8],
v2_version: u8,
known_frames: Option<HashMap<String, String>>,
) -> Result<ID3v1FindResult> {
if v2_version != 3 && v2_version != 4 {
return Err(AudexError::InvalidData(
"Only 3 and 4 possible for v2_version".to_string(),
));
}
let extra_read = 3;
if data.len() < 128 + extra_read {
if data.len() >= 124 {
let parse_data = &data[0..];
if let Some(tag_idx) = find_tag_in_data(parse_data) {
if let Ok(tag) = parse_id3v1(parse_data, tag_idx, v2_version, &known_frames) {
if tag.is_some() {
let offset = (tag_idx as i64) - (data.len() as i64);
return Ok((tag, offset));
}
}
}
}
return Ok((None, 0));
}
let start_pos = data.len() - 128 - extra_read;
let search_data = &data[start_pos..];
if let Some(idx) = find_tag_in_data(search_data) {
if let Some(ape_idx) = search_data.windows(8).position(|w| w == b"APETAGEX") {
if idx == ape_idx + extra_read {
return Ok((None, 0));
}
}
if let Ok(tag) = parse_id3v1(search_data, idx, v2_version, &known_frames) {
if tag.is_some() {
let offset = (idx as i64) - (search_data.len() as i64);
return Ok((tag, offset));
}
}
}
Ok((None, 0))
}
pub fn find_id3v1_from_reader<R: std::io::Read + std::io::Seek + ?Sized>(
reader: &mut R,
v2_version: u8,
known_frames: Option<HashMap<String, String>>,
) -> Result<ID3v1FindResult> {
use std::io::SeekFrom;
let end = reader.seek(SeekFrom::End(0))?;
let tail_size = std::cmp::min(end, 256) as usize;
if tail_size == 0 {
return Ok((None, 0));
}
reader.seek(SeekFrom::End(-(tail_size as i64)))?;
let mut tail = vec![0u8; tail_size];
reader.read_exact(&mut tail)?;
find_id3v1(&tail, v2_version, known_frames)
}
fn find_tag_in_data(data: &[u8]) -> Option<usize> {
let mut search_from = 0;
while search_from < data.len() {
let candidate = data[search_from..].windows(3).position(|w| w == b"TAG");
let idx = match candidate {
Some(pos) => search_from + pos,
None => return None,
};
let tag_data = &data[idx..];
if validate_tag_heuristics(tag_data) {
return Some(idx);
}
search_from = idx + 1;
}
None
}
fn validate_tag_heuristics(tag_data: &[u8]) -> bool {
if tag_data.len() < 124 {
return false;
}
if tag_data.len() >= 128 {
let genre = tag_data[127];
if genre > 191 && genre != 255 {
return false;
}
}
let title_field = &tag_data[3..33.min(tag_data.len())];
let non_null: Vec<u8> = title_field.iter().copied().filter(|&b| b != 0).collect();
if !non_null.is_empty() {
let control_count = non_null
.iter()
.filter(|&&b| (0x01..=0x1F).contains(&b) || (0x7F..=0x9F).contains(&b))
.count();
if control_count > non_null.len() / 2 {
return false;
}
}
true
}
fn get_frame_class_map(known_frames: &Option<HashMap<String, String>>) -> HashMap<String, bool> {
let mut frame_class = HashMap::new();
let frame_keys = vec![
"TIT2", "TPE1", "TALB", "TYER", "TDRC", "COMM", "TRCK", "TCON",
];
for key in frame_keys {
if let Some(known) = known_frames {
frame_class.insert(key.to_string(), known.contains_key(key));
} else {
frame_class.insert(key.to_string(), true);
}
}
frame_class
}
fn parse_id3v1_from_data(data: &[u8], year_field_size: usize) -> Result<ID3v1Tag> {
if data.len() < 124 {
return Err(AudexError::InvalidData(format!(
"ID3v1 data too short ({} bytes, minimum 124)",
data.len()
)));
}
if &data[0..3] != b"TAG" {
return Err(AudexError::InvalidData(
"Invalid ID3v1 tag header".to_string(),
));
}
let title = extract_field(&data[3..33]);
let artist = extract_field(&data[33..63]);
let album = extract_field(&data[63..93]);
let year_end = 93 + year_field_size;
let year = extract_field(&data[93..year_end]);
let comment_start = year_end;
let comment_end = (comment_start + 30).min(data.len());
let comment = extract_field(&data[comment_start..comment_end]);
let (track, genre) = if data.len() == 128 {
let track_byte = data[126];
let genre_byte = data[127];
let track = if data[125] == 0 && track_byte != 0 {
Some(track_byte)
} else {
None
};
(track, genre_byte)
} else {
(None, 255) };
Ok(ID3v1Tag {
title,
artist,
album,
year,
comment,
track,
genre,
})
}
pub fn parse_id3v1_to_frames(
data: &[u8],
v2_version: u8,
) -> Result<HashMap<String, Box<dyn crate::id3::frames::Frame>>> {
if let Some(idx) = find_tag_in_data(data) {
if let Ok(Some(frames)) = parse_id3v1(data, idx, v2_version, &None) {
return Ok(frames);
}
}
Err(AudexError::InvalidData(
"No valid ID3v1 tag found".to_string(),
))
}
fn parse_id3v1(
data: &[u8],
idx: usize,
v2_version: u8,
known_frames: &Option<HashMap<String, String>>,
) -> Result<Option<HashMap<String, Box<dyn crate::id3::frames::Frame>>>> {
if v2_version != 3 && v2_version != 4 {
return Err(AudexError::InvalidData(
"Only 3 and 4 possible for v2_version".to_string(),
));
}
let tag_data = &data[idx..];
if tag_data.len() > 128 || tag_data.len() < 124 {
return Ok(None);
}
let year_field_size = tag_data.len() - 124;
let id3v1_tag = parse_id3v1_from_data(tag_data, year_field_size)?;
let mut frames: HashMap<String, Box<dyn crate::id3::frames::Frame>> = HashMap::new();
let frame_class_enabled = get_frame_class_map(known_frames);
if !id3v1_tag.title.is_empty() && *frame_class_enabled.get("TIT2").unwrap_or(&true) {
let mut frame = TextFrame::new("TIT2".to_string(), vec![id3v1_tag.title]);
frame.encoding = TextEncoding::Latin1; frames.insert("TIT2".to_string(), Box::new(frame));
}
if !id3v1_tag.artist.is_empty() && *frame_class_enabled.get("TPE1").unwrap_or(&true) {
let mut frame = TextFrame::new("TPE1".to_string(), vec![id3v1_tag.artist]);
frame.encoding = TextEncoding::Latin1; frames.insert("TPE1".to_string(), Box::new(frame));
}
if !id3v1_tag.album.is_empty() && *frame_class_enabled.get("TALB").unwrap_or(&true) {
let mut frame = TextFrame::new("TALB".to_string(), vec![id3v1_tag.album]);
frame.encoding = TextEncoding::Latin1; frames.insert("TALB".to_string(), Box::new(frame));
}
if !id3v1_tag.year.is_empty() {
if v2_version == 3 && *frame_class_enabled.get("TYER").unwrap_or(&true) {
let mut frame = TextFrame::new("TYER".to_string(), vec![id3v1_tag.year]);
frame.encoding = TextEncoding::Latin1; frames.insert("TYER".to_string(), Box::new(frame));
} else if *frame_class_enabled.get("TDRC").unwrap_or(&true) {
let mut frame = TextFrame::new("TDRC".to_string(), vec![id3v1_tag.year]);
frame.encoding = TextEncoding::Latin1; frames.insert("TDRC".to_string(), Box::new(frame));
}
}
if !id3v1_tag.comment.is_empty() && *frame_class_enabled.get("COMM").unwrap_or(&true) {
let comm_frame = COMM::new(
TextEncoding::Latin1,
*b"eng",
"ID3v1 Comment".to_string(),
id3v1_tag.comment,
);
frames.insert("COMM".to_string(), Box::new(comm_frame));
}
if let Some(track) = id3v1_tag.track {
if *frame_class_enabled.get("TRCK").unwrap_or(&true)
&& ((track != 32) || (tag_data.len() >= 3 && tag_data[tag_data.len() - 3] == 0))
{
let mut frame = TextFrame::new("TRCK".to_string(), vec![track.to_string()]);
frame.encoding = TextEncoding::Latin1; frames.insert("TRCK".to_string(), Box::new(frame));
}
}
if id3v1_tag.genre != 255 && *frame_class_enabled.get("TCON").unwrap_or(&true) {
let mut tcon_frame = TCON::new("TCON".to_string(), vec![id3v1_tag.genre.to_string()]);
tcon_frame.encoding = TextEncoding::Latin1; frames.insert("TCON".to_string(), Box::new(tcon_frame));
}
Ok(Some(frames))
}
pub fn make_id3v1_from_frames(
frames: &HashMap<String, Box<dyn crate::id3::frames::Frame>>,
) -> [u8; 128] {
let mut v1_data = [0u8; 128];
v1_data[0..3].copy_from_slice(b"TAG");
let field_mappings = [
("TIT2", 3, 30), ("TPE1", 33, 30), ("TALB", 63, 30), ];
for (frame_id, start, len) in field_mappings.iter() {
if let Some(frame) = frames.get(&frame_id.to_string()) {
if let Some(text) = extract_text_from_frame(frame.as_ref()) {
let text_bytes = encode_latin1(&text);
let copy_len = text_bytes.len().min(*len);
v1_data[*start..*start + copy_len].copy_from_slice(&text_bytes[..copy_len]);
}
}
}
if let Some(tdrc_frame) = frames.get("TDRC") {
if let Some(year_text) = extract_text_from_frame(tdrc_frame.as_ref()) {
let trimmed_year = year_text.trim();
let year_bytes = encode_latin1(trimmed_year);
let copy_len = year_bytes.len().min(4);
v1_data[93..93 + copy_len].copy_from_slice(&year_bytes[..copy_len]);
}
} else if let Some(tyer_frame) = frames.get("TYER") {
if let Some(year_text) = extract_text_from_frame(tyer_frame.as_ref()) {
let trimmed_year = year_text.trim();
let year_bytes = encode_latin1(trimmed_year);
let copy_len = year_bytes.len().min(4);
v1_data[93..93 + copy_len].copy_from_slice(&year_bytes[..copy_len]);
}
}
let mut comment_len = 30; let mut track_num = None;
if let Some(trck_frame) = frames.get("TRCK") {
if let Some(track_text) = extract_text_from_frame(trck_frame.as_ref()) {
let track_part = if let Some(slash_pos) = track_text.find('/') {
&track_text[..slash_pos]
} else {
&track_text
};
if let Ok(track) = track_part.parse::<u8>() {
if track > 0 && track < 255 {
track_num = Some(track);
comment_len = 28; }
}
}
}
if let Some(comm_frame) = frames.get("COMM") {
if let Some(comment_text) = extract_text_from_frame(comm_frame.as_ref()) {
let comment_bytes = encode_latin1(&comment_text);
let copy_len = comment_bytes.len().min(comment_len);
v1_data[97..97 + copy_len].copy_from_slice(&comment_bytes[..copy_len]);
}
}
if let Some(track) = track_num {
v1_data[125] = 0; v1_data[126] = track; }
if let Some(tcon_frame) = frames.get("TCON") {
if let Some(genre_text) = extract_text_from_frame(tcon_frame.as_ref()) {
let clean_genre = if genre_text.starts_with('(') {
if let Some(close) = genre_text.find(')') {
&genre_text[1..close]
} else {
&genre_text
}
} else {
&genre_text
};
if let Ok(genre_id) = clean_genre.parse::<u8>() {
if genre_id < 192 {
v1_data[127] = genre_id;
} else {
v1_data[127] = 255;
}
} else if let Some(genre_id) = constants::find_genre_id(clean_genre) {
if genre_id < 192 {
v1_data[127] = genre_id;
} else {
v1_data[127] = 255;
}
} else {
v1_data[127] = 255;
}
} else {
v1_data[127] = 255; }
} else {
v1_data[127] = 255; }
v1_data
}
pub fn make_id3v1_from_dict(
dict: &std::collections::BTreeMap<String, Box<dyn crate::id3::frames::Frame>>,
) -> [u8; 128] {
let mut v1_data = [0u8; 128];
v1_data[0..3].copy_from_slice(b"TAG");
let field_mappings = [
("TIT2", 3, 30), ("TPE1", 33, 30), ("TALB", 63, 30), ];
for (frame_id, start, len) in field_mappings.iter() {
if let Some(frame) = dict.get(&frame_id.to_string()) {
if let Some(text) = extract_text_from_frame(frame.as_ref()) {
let text_bytes = encode_latin1(&text);
let copy_len = text_bytes.len().min(*len);
v1_data[*start..*start + copy_len].copy_from_slice(&text_bytes[..copy_len]);
}
}
}
if let Some(tdrc_frame) = dict.get("TDRC") {
if let Some(year_text) = extract_text_from_frame(tdrc_frame.as_ref()) {
let trimmed_year = year_text.trim();
let year_bytes = encode_latin1(trimmed_year);
let copy_len = year_bytes.len().min(4);
v1_data[93..93 + copy_len].copy_from_slice(&year_bytes[..copy_len]);
}
} else if let Some(tyer_frame) = dict.get("TYER") {
if let Some(year_text) = extract_text_from_frame(tyer_frame.as_ref()) {
let trimmed_year = year_text.trim();
let year_bytes = encode_latin1(trimmed_year);
let copy_len = year_bytes.len().min(4);
v1_data[93..93 + copy_len].copy_from_slice(&year_bytes[..copy_len]);
}
}
let mut comment_len = 30; let mut track_num = None;
if let Some(trck_frame) = dict.get("TRCK") {
if let Some(track_text) = extract_text_from_frame(trck_frame.as_ref()) {
let track_part = if let Some(slash_pos) = track_text.find('/') {
&track_text[..slash_pos]
} else {
&track_text
};
if let Ok(track) = track_part.parse::<u8>() {
if track > 0 && track < 255 {
track_num = Some(track);
comment_len = 28;
}
}
}
}
let comm_frame = dict.get("COMM::eng").or_else(|| {
dict.keys()
.find(|k| k.starts_with("COMM"))
.and_then(|k| dict.get(k))
});
if let Some(frame) = comm_frame {
if let Some(comment_text) = extract_text_from_frame(frame.as_ref()) {
let comment_bytes = encode_latin1(&comment_text);
let copy_len = comment_bytes.len().min(comment_len);
v1_data[97..97 + copy_len].copy_from_slice(&comment_bytes[..copy_len]);
}
}
if let Some(track) = track_num {
v1_data[125] = 0;
v1_data[126] = track;
}
if let Some(tcon_frame) = dict.get("TCON") {
if let Some(genre_text) = extract_text_from_frame(tcon_frame.as_ref()) {
let clean_genre = if genre_text.starts_with('(') {
if let Some(close) = genre_text.find(')') {
&genre_text[1..close]
} else {
&genre_text
}
} else {
&genre_text
};
if let Ok(genre_id) = clean_genre.parse::<u8>() {
if genre_id < 192 {
v1_data[127] = genre_id;
} else {
v1_data[127] = 255;
}
} else if let Some(genre_id) = constants::find_genre_id(clean_genre) {
if genre_id < 192 {
v1_data[127] = genre_id;
} else {
v1_data[127] = 255;
}
} else {
v1_data[127] = 255;
}
} else {
v1_data[127] = 255;
}
} else {
v1_data[127] = 255;
}
v1_data
}
fn extract_text_from_frame(frame: &dyn crate::id3::frames::Frame) -> Option<String> {
if let Some(values) = frame.text_values() {
if !values.is_empty() {
return Some(values[0].clone());
}
}
let description = frame.description();
description
.find(": ")
.map(|colon_pos| description[colon_pos + 2..].to_string())
}