#[warn(missing_docs)]
#[macro_use]
extern crate failure;
extern crate isolang;
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;
use failure::ResultExt;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde::de::Error as DeError;
use std::collections::HashMap;
use std::result;
#[derive(Debug, Fail)]
pub enum Error {
#[fail(display = "could not parse metadata")]
CouldNotParseMetadata,
#[fail(display = "path {:?} is not allowed", path)]
InvalidPath {
path: String,
},
#[fail(display = "beginning of time span {},{} is greater than end", begin, end)]
InvalidSpan {
begin: f32,
end: f32,
},
#[fail(display = "unsupported track type {:?} (did you want to prefix it with \"x-\"?)", value)]
UnsupportedTrackType {
value: String,
},
}
pub type Result<T> = result::Result<T, Error>;
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Metadata {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_track: Option<Track>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tracks: Vec<Track>,
pub alignments: Vec<Alignment>,
#[serde(default, skip_serializing_if = "ExtensionData::is_empty")]
pub ext: ExtensionData,
#[serde(default, skip_serializing)]
_placeholder: (),
}
impl Metadata {
pub fn from_bytes(data: &[u8]) -> result::Result<Metadata, failure::Error> {
Ok(serde_json::from_slice(data).context(Error::CouldNotParseMetadata)?)
}
pub fn from_str(data: &str) -> result::Result<Metadata, failure::Error> {
Self::from_bytes(data.as_bytes())
}
}
#[test]
fn parse_metadata() {
let examples = &[
include_str!("../fixtures/examples/book_example.aligned/metadata.json"),
include_str!("../fixtures/examples/subtitle_example.aligned/metadata.json"),
include_str!("../fixtures/examples/subtitle_extracted_example.aligned/metadata.json"),
];
for example in examples {
Metadata::from_str(example)
.expect("failed to parse example metadata");
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Track {
#[serde(rename = "type")]
pub track_type: TrackType,
#[serde(default, skip_serializing_if = "Option::is_none", with = "iso_short_code_serialization::opt")]
pub lang: Option<isolang::Language>,
file: Option<FilePath>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub html: Option<String>,
#[serde(default, skip_serializing_if = "ExtensionData::is_empty")]
pub ext: ExtensionData,
#[serde(default, skip_serializing)]
_placeholder: (),
}
impl Track {
pub fn with_type(track_type: TrackType) -> Track {
Track {
track_type: track_type,
lang: None,
file: None,
html: None,
ext: ExtensionData::default(),
_placeholder: (),
}
}
pub fn html<S: Into<String>>(lang: isolang::Language, html: S) -> Track {
Track {
track_type: TrackType::Html,
lang: Some(lang),
file: None,
html: Some(html.into()),
ext: ExtensionData::default(),
_placeholder: (),
}
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub enum TrackType {
Html,
Media,
Image,
Ext(String),
}
impl<'de> Deserialize<'de> for TrackType {
fn deserialize<D: Deserializer<'de>>(d: D) -> result::Result<Self, D::Error> {
let value: &str = Deserialize::deserialize(d)?;
match value {
"html" => Ok(TrackType::Html),
"media" => Ok(TrackType::Media),
"image" => Ok(TrackType::Image),
other if other.starts_with("x-") => {
Ok(TrackType::Ext(other[2..].to_owned()))
}
other => {
Err(D::Error::custom(Error::UnsupportedTrackType {
value: other.to_owned(),
}))
}
}
}
}
impl Serialize for TrackType {
fn serialize<S>(&self, serializer: S) -> result::Result<S::Ok, S::Error>
where
S: Serializer,
{
match *self {
TrackType::Html => "html".serialize(serializer),
TrackType::Media => "media".serialize(serializer),
TrackType::Image => "image".serialize(serializer),
TrackType::Ext(ref name) => {
format!("x-{}", name).serialize(serializer)
}
}
}
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Alignment {
#[serde(default, skip_serializing_if = "Option::is_none")]
span: Option<TimeSpan>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tracks: Vec<Track>,
#[serde(default, skip_serializing_if = "ExtensionData::is_empty")]
pub ext: ExtensionData,
#[serde(default, skip_serializing)]
_placeholder: (),
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct TimeSpan {
begin: f32,
end: f32,
}
impl TimeSpan {
pub fn new(begin: f32, end: f32) -> Result<TimeSpan> {
if begin < end {
Ok(TimeSpan { begin, end })
} else {
Err(Error::InvalidSpan { begin, end })
}
}
pub fn begin(&self) -> f32 {
self.begin
}
pub fn end(&self) -> f32 {
self.end
}
}
impl<'de> Deserialize<'de> for TimeSpan {
fn deserialize<D: Deserializer<'de>>(d: D) -> result::Result<Self, D::Error> {
let (begin, end) = Deserialize::deserialize(d)?;
TimeSpan::new(begin, end).map_err(D::Error::custom)
}
}
impl Serialize for TimeSpan {
fn serialize<S>(&self, serializer: S) -> result::Result<S::Ok, S::Error>
where
S: Serializer,
{
(self.begin, self.end).serialize(serializer)
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub struct FilePath {
path: String,
}
impl FilePath {
pub fn new<S: Into<String>>(path: S) -> Result<FilePath> {
let path = path.into();
for component in path.split("/") {
if component == "" || component == "." || component == ".."
|| component.contains("\\")
{
return Err(Error::InvalidPath { path: path.clone() });
}
}
Ok(FilePath { path })
}
}
#[test]
fn file_path_checks_validity() {
assert!(FilePath::new("good.txt").is_ok());
assert!(FilePath::new("dir/good.txt").is_ok());
assert!(FilePath::new("").is_err());
assert!(FilePath::new(".").is_err());
assert!(FilePath::new("..").is_err());
assert!(FilePath::new("dir/..").is_err());
assert!(FilePath::new("/absolute").is_err());
assert!(FilePath::new("trailing/").is_err());
assert!(FilePath::new("dir//file").is_err());
assert!(FilePath::new("back\\slash").is_err());
}
impl<'de> Deserialize<'de> for FilePath {
fn deserialize<D: Deserializer<'de>>(d: D) -> result::Result<Self, D::Error> {
let path: String = Deserialize::deserialize(d)?;
FilePath::new(path).map_err(D::Error::custom)
}
}
impl Serialize for FilePath {
fn serialize<S>(&self, serializer: S) -> result::Result<S::Ok, S::Error>
where
S: Serializer,
{
self.path.serialize(serializer)
}
}
pub type ExtensionData = HashMap<String, serde_json::Value>;
pub mod iso_short_code_serialization {
pub mod opt {
use isolang::Language;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde::de::Error;
use std::result::Result;
pub fn deserialize<'de, D>(d: D) -> Result<Option<Language>, D::Error>
where
D: Deserializer<'de>,
{
let code: Option<&str> = Deserialize::deserialize(d)?;
match code {
None => Ok(None),
Some(c) => {
let lang = Language::from_639_3(c)
.or_else(|| Language::from_639_1(c))
.ok_or_else(|| {
D::Error::unknown_variant(
c,
&["an ISO 639-1 or 639-3 language code"],
)
})?;
Ok(Some(lang))
}
}
}
pub fn serialize<S>(
lang: &Option<Language>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let code = lang.map(|l| l.to_639_1().unwrap_or_else(|| l.to_639_3()));
code.serialize(serializer)
}
}
}