use std::io::{self, Read, Write};
#[derive(thiserror::Error, Debug)]
pub enum MetadataError {
#[error("Invalid UTF-8 sequence.")]
UnicodeError(#[from] std::string::FromUtf8Error),
#[error("I/O error: {0}")]
IoError(#[from] io::Error),
#[error("Failed to read a LEB128 integer: {0}.")]
Leb128Error(#[from] leb128::read::Error),
#[error("Invalid length: {0}.")]
InvalidLength(u64),
#[error("Invalid tag.")]
InvalidByteTag(u8),
}
#[derive(num_enum::TryFromPrimitive, num_enum::IntoPrimitive, Debug, Copy, Clone)]
#[repr(u8)]
pub(crate) enum ByteTag {
Title = 1,
Author = 2,
Language = 3,
Date = 4,
License = 5,
Keyword = 6,
User = 100,
}
#[derive(Debug, PartialEq, Eq, Clone)]
#[non_exhaustive]
pub enum MetadataEntry {
Title(String),
Author(String),
Language(String),
Date(u64),
License(String),
Keyword(String),
User(String, String),
}
pub(crate) fn dump<'a, O, M>(mut output: O, metadata: M) -> io::Result<()>
where
O: Write,
M: IntoIterator<Item = &'a MetadataEntry>,
{
for entry in metadata.into_iter() {
macro_rules! w {
($tag:ident, $($values:expr),*) => {{
let tag: u8 = ByteTag::$tag.into();
debug_assert!(tag != 0);
output.write_all(&[tag])?;
$(
let bytes = $values;
leb128::write::unsigned(&mut output, bytes.len() as u64)?;
output.write_all(bytes)?;
)*
}}
}
match entry {
MetadataEntry::Title(s) => w!(Title, s.as_bytes()),
MetadataEntry::Author(s) => w!(Author, s.as_bytes()),
MetadataEntry::Language(s) => w!(Language, s.as_bytes()),
MetadataEntry::Date(d) => w!(Date, &d.to_be_bytes()),
MetadataEntry::License(s) => w!(License, s.as_bytes()),
MetadataEntry::Keyword(s) => w!(Keyword, s.as_bytes()),
MetadataEntry::User(k, v) => w!(User, k.as_bytes(), v.as_bytes()),
}
}
output.write_all(&[0])?;
Ok(())
}
pub(crate) fn load<I>(
input: I,
input_len: u64,
) -> impl Iterator<Item = Result<MetadataEntry, MetadataError>>
where
I: Read,
{
BinaryDataParser {
input,
input_len,
io_valid: true,
}
}
struct BinaryDataParser<I> {
input: I,
input_len: u64,
io_valid: bool,
}
impl<I: Read> Iterator for BinaryDataParser<I> {
type Item = Result<MetadataEntry, MetadataError>;
fn next(&mut self) -> Option<Self::Item> {
if !self.io_valid {
return None;
}
macro_rules! run {
($io:expr) => {
match $io {
Ok(result) => result,
Err(e) => {
self.io_valid = false;
return Some(Err(e.into()));
}
}
};
}
let mut byte_tag = [0xFF];
run!(self.input.read_exact(&mut byte_tag));
if byte_tag[0] == 0 {
return None;
}
let key =
run!(ByteTag::try_from(byte_tag[0])
.map_err(|_| MetadataError::InvalidByteTag(byte_tag[0])));
macro_rules! next_value {
() => {{
let value_len = run!(leb128::read::unsigned(&mut self.input));
if value_len > self.input_len {
self.io_valid = false;
return Some(Err(MetadataError::InvalidLength(value_len)));
}
let mut value_bytes = vec![0; value_len as usize];
run!(self.input.read_exact(&mut value_bytes));
value_bytes
}};
}
macro_rules! next_str {
() => {
run!(String::from_utf8(next_value!()).map_err(MetadataError::UnicodeError))
};
}
let item = match key {
ByteTag::Title => Ok(MetadataEntry::Title(next_str!())),
ByteTag::Author => Ok(MetadataEntry::Author(next_str!())),
ByteTag::Language => Ok(MetadataEntry::Language(next_str!())),
ByteTag::License => Ok(MetadataEntry::License(next_str!())),
ByteTag::Keyword => Ok(MetadataEntry::Keyword(next_str!())),
ByteTag::User => Ok(MetadataEntry::User(next_str!(), next_str!())),
ByteTag::Date => next_value!()
.try_into()
.map(|b| MetadataEntry::Date(u64::from_be_bytes(b)))
.map_err(|e| MetadataError::InvalidLength(e.len() as u64)),
};
Some(item)
}
}
#[test]
fn write_read_metadata() {
let entries = [
MetadataEntry::Title("title".into()),
MetadataEntry::Date(1234567890),
MetadataEntry::User("key".into(), "value".into()),
];
let mut buf = Vec::new();
dump(io::Cursor::new(&mut buf), &entries).unwrap();
let loaded = load(io::Cursor::new(&buf), buf.len() as u64)
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(entries, loaded[..]);
}