use std::collections::HashMap;
use std::convert::TryFrom;
use fbxscii::ElementAttribute;
use crate::{OwnedObject, objects::AttrExtractorExt};
use super::{FbxObjectTag, FbxTryFromReason, FbxTypeMismatch, fbx_object_tag};
const TYPE_ATTR: &str = "Type";
const FILE_NAME_ATTR: &str = "FileName";
const RELATIVE_FILENAME_ATTR: &str = "RelativeFilename";
const CONTENT_ATTR: &str = "Content";
#[derive(Debug, PartialEq)]
pub struct Video {
object: OwnedObject,
pub video_type: String,
pub file_name: String,
pub relative_file_name: Option<String>,
pub content: Option<Vec<u8>>,
}
impl Video {
pub fn inner(&self) -> &OwnedObject {
&self.object
}
pub fn into_inner(self) -> OwnedObject {
self.object
}
}
fn decode_optional_content(
attrs: &HashMap<String, ElementAttribute>,
) -> Result<Option<Vec<u8>>, FbxTryFromReason> {
let Some(tokens) = attrs.optional_tokens_case_insensitive(CONTENT_ATTR)? else {
return Ok(None);
};
if tokens.is_empty() {
return Ok(None);
}
let mut out = Vec::new();
for (i, t) in tokens.iter().enumerate() {
let s = t.trim();
let payload = if s.len() >= 2 && s.starts_with('"') && s.ends_with('"') {
&s[1..s.len() - 1]
} else {
s
};
let decoded =
base64::decode(payload).map_err(|e| FbxTryFromReason::InvalidAttributeFormat {
name: CONTENT_ATTR.to_string(),
detail: format!("base64 decode (token {i}): {e}"),
})?;
if decoded.is_empty() {
return Err(FbxTryFromReason::InvalidAttributeFormat {
name: CONTENT_ATTR.to_string(),
detail: format!("base64 token {i} decoded to empty"),
});
}
out.extend_from_slice(&decoded);
}
Ok(Some(out))
}
impl TryFrom<OwnedObject> for Video {
type Error = FbxTypeMismatch;
fn try_from(o: OwnedObject) -> Result<Self, Self::Error> {
if fbx_object_tag(&o) != FbxObjectTag::Video {
return Err(FbxTypeMismatch::wrong_object_kind(o, "Video".to_string()));
}
let attrs = &o.attributes;
let video_type = match attrs.require_token_case_insensitive(TYPE_ATTR) {
Ok(s) => s.to_string(),
Err(reason) => return Err(FbxTypeMismatch { object: o, reason }),
};
let file_name = match attrs.require_token_case_insensitive(FILE_NAME_ATTR) {
Ok(s) => s.to_string(),
Err(reason) => return Err(FbxTypeMismatch { object: o, reason }),
};
let relative_file_name = match attrs.optional_token_case_insensitive(RELATIVE_FILENAME_ATTR)
{
Ok(r) => r.map(|s| s.to_string()),
Err(reason) => return Err(FbxTypeMismatch { object: o, reason }),
};
let content = match decode_optional_content(attrs) {
Ok(c) => c,
Err(reason) => return Err(FbxTypeMismatch { object: o, reason }),
};
Ok(Video {
object: o,
video_type,
file_name,
relative_file_name,
content,
})
}
}
#[cfg(test)]
mod tests {
use fbxscii::{ElementAttribute, LeafAttribute};
use super::*;
#[test]
fn decode_content_quoted_base64() {
let mut attrs = HashMap::new();
attrs.insert(
"Content".to_string(),
ElementAttribute::Leaf(Box::new(LeafAttribute {
key: "Content".into(),
tokens: vec!["\"aGVsbG8=\"".into()],
})),
);
let out = decode_optional_content(&attrs).unwrap().unwrap();
assert_eq!(out, b"hello");
}
#[test]
fn decode_content_multipart_concat() {
let mut attrs = HashMap::new();
attrs.insert(
"content".to_string(),
ElementAttribute::Leaf(Box::new(LeafAttribute {
key: "content".into(),
tokens: vec!["aGVs".into(), "bG8=".into()],
})),
);
let out = decode_optional_content(&attrs).unwrap().unwrap();
assert_eq!(out, b"hello");
}
#[test]
fn decode_content_missing() {
let attrs = HashMap::new();
assert!(decode_optional_content(&attrs).unwrap().is_none());
}
}