From 3a8069d1a46578c810f24c7b1b092251ffc22c39 Mon Sep 17 00:00:00 2001 From: tomsmeding Date: Thu, 9 Jan 2020 12:05:15 +0100 Subject: Even more stuff --- src/encoding.rs | 14 ++++----- src/error.rs | 15 ++------- src/id3v2.rs | 95 +++++++++++++++++++++++++++++++++++++++++++++------------ src/main.rs | 86 ++++++++++++++++++++++++++++----------------------- src/options.rs | 4 --- 5 files changed, 132 insertions(+), 82 deletions(-) diff --git a/src/encoding.rs b/src/encoding.rs index a3851aa..c117cd0 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -42,10 +42,10 @@ pub fn from_ucs_2_bom(bytes: &[u8]) -> Option { Some(res) } -// pub fn from_utf8_mistaken_as_latin1(latin1: &str) -> Option { -// guard!(latin1.chars().all(|c| (c as usize) < 256)); -// match std::str::from_utf8(latin1.as_bytes()) { -// Ok(res) => Some(res.to_string()), -// Err(_) => None, -// } -// } +pub fn from_utf8_mistaken_as_latin1(latin1: &str) -> Option { + guard!(latin1.chars().all(|c| (c as usize) < 256)); + match std::str::from_utf8(latin1.as_bytes()) { + Ok(res) => Some(res.to_string()), + Err(_) => None, + } +} diff --git a/src/error.rs b/src/error.rs index 6358bfe..19bd48d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -4,19 +4,8 @@ pub trait IntoIOError { fn ioerr(self) -> io::Error; } -impl IntoIOError for String { - fn ioerr(self) -> io::Error { - io::Error::new(io::ErrorKind::Other, self) - } -} - -impl IntoIOError for &str { - fn ioerr(self) -> io::Error { - io::Error::new(io::ErrorKind::Other, self) - } -} - -impl IntoIOError for std::string::FromUtf8Error { +// This impl bound is taken directly from the io::Error::new function. +impl>> IntoIOError for E { fn ioerr(self) -> io::Error { io::Error::new(io::ErrorKind::Other, self) } diff --git a/src/id3v2.rs b/src/id3v2.rs index a8dccb9..934bdf3 100644 --- a/src/id3v2.rs +++ b/src/id3v2.rs @@ -1,9 +1,11 @@ // http://id3.org/id3v2.3.0 +use std::convert::TryFrom; use std::io::{self, Read}; +use std::iter; +use std::num::TryFromIntError; use crate::encoding::{from_latin_1, from_ucs_2_bom}; use crate::error::IntoIOError; -use crate::options::EncodingOptions; use crate::util::read_big_endian; fn parse_id3v2_header(bytes: &[u8]) -> Option<(u16, u8, usize)> { @@ -46,6 +48,30 @@ pub enum Frame { TRCK(String), } +struct ArrayIter2 { + arr: [T; 2], + cursor: u8, +} + +impl ArrayIter2 { + fn new(arr: [T; 2]) -> Self { + Self { arr, cursor: 0 } + } +} + +impl Iterator for ArrayIter2 { + type Item = T; + fn next(&mut self) -> Option { + if (self.cursor as usize) < self.arr.len() { + let i = self.cursor; + self.cursor += 1; + Some(self.arr[i as usize].clone()) + } else { + None + } + } +} + impl RawFrame { fn parse(data: &[u8]) -> Result, String> { if data.len() < 10 { @@ -69,16 +95,12 @@ impl RawFrame { Ok(Some((RawFrame { id, flags, body }, 10 + size))) } - fn interpret_encoded_string(&self, encopts: &EncodingOptions) -> io::Result { + fn interpret_encoded_string(&self) -> io::Result { match self.body.get(0).ok_or("String field too small".ioerr())? { 0 => { // Latin-1 let mut i = self.body.len(); while i > 0 && self.body[i-1] == 0 { i -= 1; } - if encopts.latin1_as_utf8 { - String::from_utf8(self.body[1..i].to_vec()).map_err(|e| e.ioerr()) - } else { - from_latin_1(&self.body[1..i]).ok_or("Invalid Latin-1 string field".ioerr()) - } + from_latin_1(&self.body[1..i]).ok_or("Invalid Latin-1 string field".ioerr()) } 1 => { // UCS-2 @@ -93,21 +115,56 @@ impl RawFrame { } } - pub fn interpret(&self, encopts: &EncodingOptions) -> io::Result> { - if self.id == "TIT2" { - self.interpret_encoded_string(encopts).map(Frame::TIT2).map(Some) - } else if self.id == "TYER" { - self.interpret_encoded_string(encopts).map(Frame::TYER).map(Some) - } else if self.id == "TPE1" { - self.interpret_encoded_string(encopts).map(Frame::TPE1).map(Some) - } else if self.id == "TALB" { - self.interpret_encoded_string(encopts).map(Frame::TALB).map(Some) - } else if self.id == "TRCK" { - self.interpret_encoded_string(encopts).map(Frame::TRCK).map(Some) - } else { + fn encode_string(s: &str) -> io::Result> { + let as_latin1 = || iter::once(Ok(0)) + .chain(s.chars().map(|c| u8::try_from(u32::from(c)))) + .collect::, _>>(); + + let as_ucs2 = || { + let nibbles = s.chars() + .map(|c| u16::try_from(u32::from(c))) + .collect::, _>>()?; + Ok(iter::once(1) + .chain(nibbles.iter().flat_map(|&n| ArrayIter2::new([(n >> 8) as u8, n as u8]))) + .collect::>()) + }; + + as_latin1() + .or_else(|_| as_ucs2()) + .map_err(|e: TryFromIntError| e.ioerr()) + } + + pub fn interpret(&self) -> io::Result> { + let type_t = |typ: fn(String) -> Frame| self.interpret_encoded_string().map(typ).map(Some); + + if self.id == "TIT2" { type_t(Frame::TIT2) } + else if self.id == "TYER" { type_t(Frame::TYER) } + else if self.id == "TPE1" { type_t(Frame::TPE1) } + else if self.id == "TALB" { type_t(Frame::TALB) } + else if self.id == "TRCK" { type_t(Frame::TRCK) } + else { Ok(None) } } + + pub fn map_string String>(self, f: F) -> io::Result> { + let type_t = |id: &str, body: String| -> io::Result { + Ok(Self { + id: id.to_string(), + flags: 0, + body: Self::encode_string(&body)?, + }) + }; + + match self.interpret()? { + Some(Frame::TIT2(s)) => Ok(Some(type_t("TIT2", f(s))?)), + Some(Frame::TYER(s)) => Ok(Some(type_t("TYER", f(s))?)), + Some(Frame::TPE1(s)) => Ok(Some(type_t("TPE1", f(s))?)), + Some(Frame::TALB(s)) => Ok(Some(type_t("TALB", f(s))?)), + Some(Frame::TRCK(s)) => Ok(Some(type_t("TRCK", f(s))?)), + None => Ok(Some(self)), + } + } } impl ID3v2 { diff --git a/src/main.rs b/src/main.rs index 3939e17..63bbbf0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,8 @@ use std::io; use std::fs::File; use argparse::{ArgumentParser, StoreTrue, Store}; -use crate::options::{EncodingOptions, Options}; +use crate::options::Options; +use crate::id3v2::{ID3v2, Frame}; mod encoding; mod error; @@ -9,57 +10,64 @@ mod id3v2; mod options; mod util; -fn main() -> io::Result<()> { - let options = { - let mut options: Options = Default::default(); +fn parse_options_into(opt: &mut Options) { + let mut ap = ArgumentParser::new(); + ap.set_description("ID3v2 tag editor/fixer. Incomplete/work-in-progress."); + + ap.refer(&mut opt.write) + .add_option(&["-w", "--write"], StoreTrue, + "Write updated information instead of just displaying. Unmodified or unknown tags are preserved as-is."); - { - let mut ap = ArgumentParser::new(); - ap.set_description("ID3v2 tag editor/fixer. Incomplete/work-in-progress."); + ap.refer(&mut opt.latin1_as_utf8) + .add_option(&["--assume-utf8"], StoreTrue, + "Assume that all strings specified in the MP3 as Latin-1 are really UTF-8."); - ap.refer(&mut options.write) - .add_option(&["-w", "--write"], StoreTrue, - "Write updated information instead of just displaying. Unmodified or unknown tags are preserved as-is."); + ap.refer(&mut opt.set_tags.album) .add_option(&["-A", "--album"], Store, "Set/replace album"); + ap.refer(&mut opt.set_tags.artist).add_option(&["-a", "--artist"], Store, "Set/replace artist"); + ap.refer(&mut opt.set_tags.title) .add_option(&["-t", "--title"], Store, "Set/replace title"); + ap.refer(&mut opt.set_tags.track) .add_option(&["-T", "--track"], Store, "Set/replace track number (num or num/num)"); + ap.refer(&mut opt.set_tags.year) .add_option(&["-y", "--year"], Store, "Set/replace year"); - ap.refer(&mut options.latin1_as_utf8) - .add_option(&["--assume-utf8"], StoreTrue, - "Assume that all strings specified in the MP3 as Latin-1 are really UTF-8."); + ap.refer(&mut opt.file) + .required() + .add_argument("file", Store, + "File to operate on (probably a .mp3)"); - ap.refer(&mut options.set_tags.album) .add_option(&["-A", "--album"], Store, "Set/replace album"); - ap.refer(&mut options.set_tags.artist).add_option(&["-a", "--artist"], Store, "Set/replace artist"); - ap.refer(&mut options.set_tags.title) .add_option(&["-t", "--title"], Store, "Set/replace title"); - ap.refer(&mut options.set_tags.track) .add_option(&["-T", "--track"], Store, "Set/replace track number (num or num/num)"); - ap.refer(&mut options.set_tags.year) .add_option(&["-y", "--year"], Store, "Set/replace year"); + ap.parse_args_or_exit(); +} - ap.refer(&mut options.file) - .required() - .add_argument("file", Store, - "File to operate on (probably a .mp3)"); +fn parse_options() -> Options { + let mut options = Default::default(); + parse_options_into(&mut options); + options +} - ap.parse_args_or_exit(); +fn print_tag(tag: &ID3v2) -> io::Result<()> { + for frame in &tag.frames { + match frame.interpret()? { + Some(Frame::TALB(album)) => println!("Album : '{}'", album), + Some(Frame::TIT2(title)) => println!("Title : '{}'", title), + Some(Frame::TPE1(artist)) => println!("Artist: '{}'", artist), + Some(Frame::TYER(year)) => println!("Year : '{}'", year), + Some(Frame::TRCK(track)) => println!("Track : '{}'", track), + None => { + println!("Unknown frame: {:?}", frame); + } } + } + Ok(()) +} - options - }; +fn main() -> io::Result<()> { + let options = parse_options(); - let mut f = File::open(options.file)?; - let tag = id3v2::ID3v2::from_stream(&mut f)?; + let tag = ID3v2::from_stream(&mut File::open(options.file)?)?; // println!("{:?}", tag); // TODO: apply changes from options here + // Use RawFrame::map_string - for frame in tag.frames { - match frame.interpret(&EncodingOptions { latin1_as_utf8: options.latin1_as_utf8 })? { - Some(id3v2::Frame::TALB(album)) => println!("Album : '{}'", album), - Some(id3v2::Frame::TIT2(title)) => println!("Title : '{}'", title), - Some(id3v2::Frame::TPE1(artist)) => println!("Artist: '{}'", artist), - Some(id3v2::Frame::TYER(year)) => println!("Year : '{}'", year), - Some(id3v2::Frame::TRCK(track)) => println!("Track : '{}'", track), - None => { - println!("Unknown frame: {:?}", frame); - } - } - } + print_tag(&tag)?; // TODO: if -w, then write tags to file (if it fits) diff --git a/src/options.rs b/src/options.rs index e51004a..ceba05d 100644 --- a/src/options.rs +++ b/src/options.rs @@ -14,10 +14,6 @@ pub struct TagOptions { pub year: String, } -pub struct EncodingOptions { - pub latin1_as_utf8: bool, -} - impl Default for Options { fn default() -> Self { Options { -- cgit v1.2.3