1use std::collections::HashMap;
7use std::convert::TryFrom;
8
9use fbxscii::ElementAttribute;
10
11use crate::{OwnedObject, objects::AttrExtractorExt};
12
13use super::{FbxObjectTag, FbxTryFromReason, FbxTypeMismatch, fbx_object_tag};
14
15const TYPE_ATTR: &str = "Type";
16const FILE_NAME_ATTR: &str = "FileName";
17const RELATIVE_FILENAME_ATTR: &str = "RelativeFilename";
18const CONTENT_ATTR: &str = "Content";
19
20#[derive(Debug, PartialEq)]
21pub struct Video {
22 object: OwnedObject,
23 pub video_type: String,
25 pub file_name: String,
27 pub relative_file_name: Option<String>,
29 pub content: Option<Vec<u8>>,
31}
32
33impl Video {
34 pub fn inner(&self) -> &OwnedObject {
35 &self.object
36 }
37
38 pub fn into_inner(self) -> OwnedObject {
39 self.object
40 }
41}
42
43fn decode_optional_content(
49 attrs: &HashMap<String, ElementAttribute>,
50) -> Result<Option<Vec<u8>>, FbxTryFromReason> {
51 let Some(tokens) = attrs.optional_tokens_case_insensitive(CONTENT_ATTR)? else {
52 return Ok(None);
53 };
54 if tokens.is_empty() {
55 return Ok(None);
56 }
57
58 let mut out = Vec::new();
60 for (i, t) in tokens.iter().enumerate() {
61 let s = t.trim();
62 let payload = if s.len() >= 2 && s.starts_with('"') && s.ends_with('"') {
64 &s[1..s.len() - 1]
65 } else {
66 s
67 };
68 let decoded =
69 base64::decode(payload).map_err(|e| FbxTryFromReason::InvalidAttributeFormat {
70 name: CONTENT_ATTR.to_string(),
71 detail: format!("base64 decode (token {i}): {e}"),
72 })?;
73 if decoded.is_empty() {
74 return Err(FbxTryFromReason::InvalidAttributeFormat {
75 name: CONTENT_ATTR.to_string(),
76 detail: format!("base64 token {i} decoded to empty"),
77 });
78 }
79 out.extend_from_slice(&decoded);
80 }
81
82 Ok(Some(out))
83}
84
85impl TryFrom<OwnedObject> for Video {
86 type Error = FbxTypeMismatch;
87
88 fn try_from(o: OwnedObject) -> Result<Self, Self::Error> {
89 if fbx_object_tag(&o) != FbxObjectTag::Video {
90 return Err(FbxTypeMismatch::wrong_object_kind(o, "Video".to_string()));
91 }
92
93 let attrs = &o.attributes;
94 let video_type = match attrs.require_token_case_insensitive(TYPE_ATTR) {
96 Ok(s) => s.to_string(),
97 Err(reason) => return Err(FbxTypeMismatch { object: o, reason }),
98 };
99
100 let file_name = match attrs.require_token_case_insensitive(FILE_NAME_ATTR) {
101 Ok(s) => s.to_string(),
102 Err(reason) => return Err(FbxTypeMismatch { object: o, reason }),
103 };
104 let relative_file_name = match attrs.optional_token_case_insensitive(RELATIVE_FILENAME_ATTR)
105 {
106 Ok(r) => r.map(|s| s.to_string()),
107 Err(reason) => return Err(FbxTypeMismatch { object: o, reason }),
108 };
109 let content = match decode_optional_content(attrs) {
110 Ok(c) => c,
111 Err(reason) => return Err(FbxTypeMismatch { object: o, reason }),
112 };
113
114 Ok(Video {
115 object: o,
116 video_type,
117 file_name,
118 relative_file_name,
119 content,
120 })
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use fbxscii::{ElementAttribute, LeafAttribute};
127
128 use super::*;
129
130 #[test]
131 fn decode_content_quoted_base64() {
132 let mut attrs = HashMap::new();
133 attrs.insert(
134 "Content".to_string(),
135 ElementAttribute::Leaf(Box::new(LeafAttribute {
136 key: "Content".into(),
137 tokens: vec!["\"aGVsbG8=\"".into()],
138 })),
139 );
140 let out = decode_optional_content(&attrs).unwrap().unwrap();
141 assert_eq!(out, b"hello");
142 }
143
144 #[test]
145 fn decode_content_multipart_concat() {
146 let mut attrs = HashMap::new();
147 attrs.insert(
148 "content".to_string(),
149 ElementAttribute::Leaf(Box::new(LeafAttribute {
150 key: "content".into(),
151 tokens: vec!["aGVs".into(), "bG8=".into()],
152 })),
153 );
154 let out = decode_optional_content(&attrs).unwrap().unwrap();
155 assert_eq!(out, b"hello");
156 }
157
158 #[test]
159 fn decode_content_missing() {
160 let attrs = HashMap::new();
161 assert!(decode_optional_content(&attrs).unwrap().is_none());
162 }
163}