1use super::message::{BiopMessage, DirectoryMessage};
12use std::collections::HashMap;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
18#[non_exhaustive]
19pub enum CarouselObject {
20 Directory(DirectoryObjectData),
22 File(FileObjectData),
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct DirectoryObjectData {
29 pub entries: Vec<(Vec<u8>, u16, Vec<u8>)>,
31 pub is_service_gateway: bool,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct FileObjectData {
38 pub content: Vec<u8>,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Hash)]
45struct ObjectKey {
46 module_id: u16,
47 object_key: Vec<u8>,
48}
49
50#[derive(Debug, Clone)]
67pub struct CarouselFs {
68 objects: HashMap<ObjectKey, CarouselObject>,
69 root_key: Option<ObjectKey>,
71}
72
73impl CarouselFs {
74 pub fn from_modules(modules: &[(u16, &[u8])]) -> Self {
79 let mut objects: HashMap<ObjectKey, CarouselObject> = HashMap::new();
80 let mut root_key: Option<ObjectKey> = None;
81
82 for &(module_id, data) in modules {
83 let mut pos = 0;
84 while pos < data.len() {
85 let remaining = &data[pos..];
86 match BiopMessage::parse_at(remaining) {
87 Ok((msg, consumed)) => {
88 let (obj_key_bytes, obj) = extract_object(module_id, &msg);
90 if let Some(obj) = obj {
91 let is_sg = matches!(&msg, BiopMessage::ServiceGateway(_));
92 let key = ObjectKey {
93 module_id,
94 object_key: obj_key_bytes,
95 };
96 if is_sg && root_key.is_none() {
97 root_key = Some(key.clone());
98 }
99 objects.insert(key, obj);
100 }
101 pos += consumed;
102 }
103 Err(_) => break,
104 }
105 }
106 }
107
108 CarouselFs { objects, root_key }
109 }
110
111 pub fn service_gateway(&self) -> Option<&CarouselObject> {
113 self.root_key.as_ref().and_then(|k| self.objects.get(k))
114 }
115
116 pub fn resolve(&self, path: &[&str]) -> Option<&CarouselObject> {
119 let mut cur_key = self.root_key.clone()?;
120 for &segment in path {
121 let dir = match self.objects.get(&cur_key)? {
122 CarouselObject::Directory(d) => d,
123 CarouselObject::File(_) => return None,
124 };
125 let (_, mod_id, key_bytes) = dir.entries.iter().find(|(name, _, _)| {
127 let n = strip_nul(name);
128 n == segment.as_bytes()
129 })?;
130 cur_key = ObjectKey {
131 module_id: *mod_id,
132 object_key: key_bytes.clone(),
133 };
134 }
135 self.objects.get(&cur_key)
136 }
137
138 pub fn file_bytes(&self, path: &[&str]) -> Option<&[u8]> {
140 match self.resolve(path)? {
141 CarouselObject::File(f) => Some(&f.content),
142 CarouselObject::Directory(_) => None,
143 }
144 }
145}
146
147fn strip_nul(name: &[u8]) -> &[u8] {
149 if name.last() == Some(&0) {
150 &name[..name.len() - 1]
151 } else {
152 name
153 }
154}
155
156fn extract_object(module_id: u16, msg: &BiopMessage<'_>) -> (Vec<u8>, Option<CarouselObject>) {
159 match msg {
160 BiopMessage::Directory(dm) | BiopMessage::ServiceGateway(dm) => {
161 let key_bytes = dm.object_key.to_vec();
162 let entries = extract_dir_entries(module_id, dm);
163 let is_sg = matches!(msg, BiopMessage::ServiceGateway(_));
164 (
165 key_bytes,
166 Some(CarouselObject::Directory(DirectoryObjectData {
167 entries,
168 is_service_gateway: is_sg,
169 })),
170 )
171 }
172 BiopMessage::File(fm) => {
173 let key_bytes = fm.object_key.to_vec();
174 let content = fm.content.to_vec();
175 (
176 key_bytes,
177 Some(CarouselObject::File(FileObjectData { content })),
178 )
179 }
180 BiopMessage::Stream(_) | BiopMessage::StreamEvent(_) => (vec![], None),
181 }
182}
183
184fn extract_dir_entries(
186 _self_module_id: u16,
187 dm: &DirectoryMessage<'_>,
188) -> Vec<(Vec<u8>, u16, Vec<u8>)> {
189 let mut entries = Vec::with_capacity(dm.bindings.len());
190 for binding in &dm.bindings {
191 let name = binding
193 .name
194 .first()
195 .map(|nc| nc.id.to_vec())
196 .unwrap_or_default();
197 if let Some(bp) = binding.ior.biop_profile() {
199 let mod_id = bp.object_location.module_id;
200 let obj_key = bp.object_location.object_key.to_vec();
201 entries.push((name, mod_id, obj_key));
202 }
203 }
204 entries
205}
206
207#[cfg(test)]
210mod tests {
211 use super::*;
212 use crate::carousel::biop::ior::NameComponent;
213 use crate::carousel::biop::{
214 ior::{BiopProfileBody, ConnBinder, Ior, ObjectLocation, TaggedProfile},
215 message::{Binding, BiopMessage, DirectoryMessage, FileMessage},
216 BINDING_NOBJECT,
217 };
218 use dvb_common::Serialize;
219
220 fn build_test_carousel() -> Vec<(u16, Vec<u8>)> {
224 let file_ior = Ior {
226 type_id: b"fil\0",
227 profiles: vec![TaggedProfile::Biop(BiopProfileBody {
228 object_location: ObjectLocation {
229 carousel_id: 0xAB,
230 module_id: 2,
231 version_major: 1,
232 version_minor: 0,
233 object_key: &[0x02],
234 },
235 conn_binder: ConnBinder { taps: vec![] },
236 extra: vec![],
237 })],
238 };
239
240 let sgw = BiopMessage::ServiceGateway(DirectoryMessage {
241 object_kind: *b"srg\0",
242 object_key: &[0x01],
243 object_info: &[],
244 service_context: &[0x00],
245 bindings: vec![Binding {
246 name: vec![NameComponent {
247 id: b"index.html",
248 kind: b"fil\0",
249 }],
250 binding_type: BINDING_NOBJECT,
251 ior: file_ior,
252 object_info: &[],
253 }],
254 });
255
256 let file = BiopMessage::File(FileMessage {
257 object_key: &[0x02],
258 content_size: 11,
259 object_info_extra: &[],
260 service_context: &[0x00],
261 content: b"hello world",
262 });
263
264 let mut mod1 = vec![0u8; sgw.serialized_len()];
265 sgw.serialize_into(&mut mod1).unwrap();
266
267 let mut mod2 = vec![0u8; file.serialized_len()];
268 file.serialize_into(&mut mod2).unwrap();
269
270 vec![(1u16, mod1), (2u16, mod2)]
271 }
272
273 #[test]
274 fn carousel_fs_resolve_file() {
275 let modules = build_test_carousel();
276 let refs: Vec<(u16, &[u8])> = modules
277 .iter()
278 .map(|(id, data)| (*id, data.as_slice()))
279 .collect();
280 let fs = CarouselFs::from_modules(&refs);
281
282 assert!(fs.service_gateway().is_some());
284
285 let content = fs.file_bytes(&["index.html"]);
287 assert_eq!(content, Some(b"hello world".as_slice()));
288 }
289
290 #[test]
291 fn carousel_fs_resolve_missing_returns_none() {
292 let modules = build_test_carousel();
293 let refs: Vec<(u16, &[u8])> = modules
294 .iter()
295 .map(|(id, data)| (*id, data.as_slice()))
296 .collect();
297 let fs = CarouselFs::from_modules(&refs);
298 assert!(fs.file_bytes(&["does-not-exist.html"]).is_none());
299 }
300}