Skip to main content

dvb_si/carousel/biop/
fs.rs

1//! Virtual filesystem view of a DVB object carousel.
2//!
3//! [`CarouselFs`] is built from a set of `(module_id, &[u8])` pairs (the
4//! reassembled module data from [`crate::carousel::ModuleReassembler`]).
5//! It walks each module's BIOP messages, indexes by `(module_id, object_key)`,
6//! and exposes a path-based resolver so callers can retrieve file content
7//! without understanding the IOR chain.
8//!
9//! Spec: `docs/iso_13818_6_biop.md` (ETSI TR 101 202 §4.7.4).
10
11use super::message::{BiopMessage, DirectoryMessage};
12use std::collections::HashMap;
13
14// ── CarouselObject ────────────────────────────────────────────────────────────
15
16/// One object parsed from a carousel module.
17#[derive(Debug, Clone, PartialEq, Eq)]
18#[non_exhaustive]
19pub enum CarouselObject {
20    /// A Directory or ServiceGateway object.
21    Directory(DirectoryObjectData),
22    /// A File object.
23    File(FileObjectData),
24}
25
26/// Owned data extracted from a `DirectoryMessage`.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct DirectoryObjectData {
29    /// Bindings in this directory: `(name_bytes_no_nul, module_id, object_key_bytes)`.
30    pub entries: Vec<(Vec<u8>, u16, Vec<u8>)>,
31    /// True if this is a ServiceGateway (the carousel root).
32    pub is_service_gateway: bool,
33}
34
35/// Owned data extracted from a `FileMessage`.
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct FileObjectData {
38    /// File content bytes.
39    pub content: Vec<u8>,
40}
41
42// ── Key type ──────────────────────────────────────────────────────────────────
43
44#[derive(Debug, Clone, PartialEq, Eq, Hash)]
45struct ObjectKey {
46    module_id: u16,
47    object_key: Vec<u8>,
48}
49
50// ── CarouselFs ────────────────────────────────────────────────────────────────
51
52/// A virtual filesystem built from a set of reassembled carousel modules.
53///
54/// # Construction
55///
56/// ```text
57/// // collect (module_id, data) pairs from ModuleReassembler:
58/// let fs = CarouselFs::from_modules(&[(1, data_bytes)]);
59/// ```
60///
61/// # Path resolution
62///
63/// Paths are `&[&str]` slices (e.g. `&["images", "logo.png"]`).
64/// The root is the ServiceGateway object; each step follows a binding name.
65/// Binding names that end in `\0` have the trailing NUL stripped before matching.
66#[derive(Debug, Clone)]
67pub struct CarouselFs {
68    objects: HashMap<ObjectKey, CarouselObject>,
69    /// Key of the ServiceGateway (root) object, if found.
70    root_key: Option<ObjectKey>,
71}
72
73impl CarouselFs {
74    /// Build a `CarouselFs` from module `(module_id, data)` pairs.
75    ///
76    /// Each module's bytes are walked via `BiopMessage::parse_at`.
77    /// Unknown message kinds are silently skipped.
78    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                        // Extract the object key and kind from the message.
89                        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    /// Return the ServiceGateway (root) object, if present.
112    pub fn service_gateway(&self) -> Option<&CarouselObject> {
113        self.root_key.as_ref().and_then(|k| self.objects.get(k))
114    }
115
116    /// Resolve a path `&[&str]` starting from the ServiceGateway root.
117    /// Returns the `CarouselObject` at that path, or `None`.
118    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            // Find binding with name matching `segment` (strip trailing NUL).
126            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    /// Resolve a path and return the file content bytes, if the target is a File.
139    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
147/// Strip a trailing NUL byte from a name slice.
148fn strip_nul(name: &[u8]) -> &[u8] {
149    if name.last() == Some(&0) {
150        &name[..name.len() - 1]
151    } else {
152        name
153    }
154}
155
156/// Extract the object key bytes and a `CarouselObject` from a parsed message.
157/// Returns `(object_key_bytes, Some(obj))` or `(vec![], None)` if not indexable.
158fn 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
184/// Extract binding entries from a DirectoryMessage as `(name, module_id, object_key)`.
185fn 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        // DVB: nameComponents_count == 1 per binding.
192        let name = binding
193            .name
194            .first()
195            .map(|nc| nc.id.to_vec())
196            .unwrap_or_default();
197        // Get the module_id and object_key from the IOR BIOP profile.
198        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// ── Tests ─────────────────────────────────────────────────────────────────────
208
209#[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    /// Build a simple carousel in memory:
221    ///   Module 1: ServiceGateway dir with one binding "index.html" → module 2, key [2]
222    ///   Module 2: File with content b"hello world"
223    fn build_test_carousel() -> Vec<(u16, Vec<u8>)> {
224        // Build the IOR pointing to module 2, key [0x02]
225        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        // Service gateway should be present
283        assert!(fs.service_gateway().is_some());
284
285        // File lookup
286        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}