summaryrefslogtreecommitdiff
path: root/src/id3v2.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/id3v2.rs')
-rw-r--r--src/id3v2.rs146
1 files changed, 146 insertions, 0 deletions
diff --git a/src/id3v2.rs b/src/id3v2.rs
new file mode 100644
index 0000000..c16c573
--- /dev/null
+++ b/src/id3v2.rs
@@ -0,0 +1,146 @@
+// http://id3.org/id3v2.3.0
+
+use std::io::{self, Read};
+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)> {
+ if bytes.len() == 10 &&
+ bytes[0] == b'I' &&
+ bytes[1] == b'D' &&
+ bytes[2] == b'3' &&
+ bytes[3] != 0xff &&
+ bytes[4] != 0xff &&
+ bytes[6..10].iter().all(|b| (b & 0x80) == 0) {
+ Some((
+ read_big_endian(&bytes[3..5], 8) as u16,
+ bytes[5],
+ read_big_endian(&bytes[6..10], 7)
+ ))
+ } else {
+ None
+ }
+}
+
+#[derive(Debug)]
+pub struct ID3v2 {
+ header_size: usize,
+ pub frames: Vec<RawFrame>,
+}
+
+#[derive(Debug)]
+pub struct RawFrame {
+ id: String,
+ flags: u16,
+ body: Vec<u8>,
+}
+
+#[derive(Debug)]
+pub enum Frame {
+ TIT2(String),
+ TYER(String),
+ TPE1(String),
+}
+
+impl RawFrame {
+ fn parse(data: &[u8]) -> Result<Option<(Self, usize)>, String> {
+ if data.len() < 10 {
+ return Err(String::from("Frame buffer too short"));
+ }
+
+ if data[0..4].iter().all(|&b| b == 0) {
+ return Ok(None)
+ }
+
+ if !data[0..4].iter().all(|&b| (b'A' <= b && b <= b'Z') || (b'0' <= b && b <= b'9')) {
+ return Err(format!("Invalid frame type {:?}", &data[0..4]));
+ }
+
+ let id = String::from_utf8(data[0..4].to_vec()).unwrap();
+ let size = read_big_endian(&data[4..8], 8);
+ let flags = read_big_endian(&data[8..10], 8) as u16;
+
+ let body = data[10..10+size].to_vec();
+
+ Ok(Some((RawFrame { id, flags, body }, 10 + size)))
+ }
+
+ fn interpret_encoded_string(&self, encopts: &EncodingOptions) -> io::Result<String> {
+ 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())
+ }
+ }
+
+ 1 => { // UCS-2
+ let mut i = self.body.len();
+ while i > 1 && self.body[i-2] == 0 && self.body[i-1] == 0 { i -= 2; }
+ from_ucs_2_bom(&self.body[1..i]).ok_or("Invalid UCS-2 string field".ioerr())
+ }
+
+ enc => {
+ Err(format!("Unknown string encoding {}", enc).ioerr())
+ }
+ }
+ }
+
+ pub fn interpret(&self, encopts: &EncodingOptions) -> io::Result<Option<Frame>> {
+ 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 {
+ Ok(None)
+ }
+ }
+}
+
+impl ID3v2 {
+ pub fn from_stream<R: Read>(stream: &mut R) -> io::Result<Self> {
+ let mut header = [0u8; 10];
+ stream.read_exact(&mut header)?;
+
+ let (id3version, flags, header_size) = parse_id3v2_header(&header).ok_or("Invalid ID3 header".ioerr())?;
+ if id3version != 0x0300 {
+ return Err(format!("ID3 header version {}.{} not supported", id3version / 256, id3version % 256).ioerr())
+ }
+
+ if flags != 0 {
+ return Err(format!("No ID3 header flags supported ({:x})", flags).ioerr());
+ }
+
+ let body = {
+ let mut body = Vec::new();
+ body.resize(header_size, 0u8);
+ stream.read_exact(&mut body)?;
+ body
+ };
+
+ let mut frames = Vec::new();
+ let mut cursor = 0;
+
+ while cursor < body.len() {
+ match RawFrame::parse(&body[cursor..]).map_err(|e| e.ioerr())? {
+ Some((frame, consumed)) => {
+ frames.push(frame);
+ cursor += consumed;
+ }
+
+ None => {
+ break;
+ }
+ }
+ }
+
+ Ok(ID3v2 { frames, header_size })
+ }
+}