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