Skip to main content

qualia_core_db/sparql_library/
sparql_mm.rs

1//! SPARQL-MM (Multimedia) Support
2//!
3//! Implements SPARQL-MM for media fragments and time-series windowing.
4//! Supports Media Annotations Ontology (MA Ontology, http://www.w3.org/ns/ma-ont#).
5//!
6//! V6 repair: MA/C2PA constants use canonical `q_hash` (no placeholder collisions);
7//! caller-buffered region/time queries; real spatial intersection; honest C2PA status.
8
9use crate::q_hash;
10use crate::sparql_ast::*;
11use crate::NQuin;
12
13/// Media Annotations Ontology predicate hashes (`q_hash` of canonical IRIs).
14pub mod ma_ont {
15    use crate::q_hash;
16
17    pub const HAS_FRAGMENT: u64 = q_hash("http://www.w3.org/ns/ma-ont#hasFragment");
18    pub const HAS_TEMPORAL_FRAGMENT: u64 =
19        q_hash("http://www.w3.org/ns/ma-ont#hasTemporalFragment");
20    pub const HAS_SPATIAL_FRAGMENT: u64 = q_hash("http://www.w3.org/ns/ma-ont#hasSpatialFragment");
21    pub const HAS_TRACK_FRAGMENT: u64 = q_hash("http://www.w3.org/ns/ma-ont#hasTrackFragment");
22
23    pub const HAS_START_TIME: u64 = q_hash("http://www.w3.org/ns/ma-ont#hasStartTime");
24    pub const HAS_END_TIME: u64 = q_hash("http://www.w3.org/ns/ma-ont#hasEndTime");
25    pub const DURATION: u64 = q_hash("http://www.w3.org/ns/ma-ont#duration");
26
27    pub const HAS_X: u64 = q_hash("http://www.w3.org/ns/ma-ont#hasX");
28    pub const HAS_Y: u64 = q_hash("http://www.w3.org/ns/ma-ont#hasY");
29    pub const HAS_WIDTH: u64 = q_hash("http://www.w3.org/ns/ma-ont#hasWidth");
30    pub const HAS_HEIGHT: u64 = q_hash("http://www.w3.org/ns/ma-ont#hasHeight");
31
32    pub const HAS_TRACK: u64 = q_hash("http://www.w3.org/ns/ma-ont#hasTrack");
33    pub const HAS_TRACK_NAME: u64 = q_hash("http://www.w3.org/ns/ma-ont#hasTrackName");
34    pub const HAS_TRACK_NUMBER: u64 = q_hash("http://www.w3.org/ns/ma-ont#hasTrackNumber");
35
36    pub const HAS_FORMAT: u64 = q_hash("http://www.w3.org/ns/ma-ont#hasFormat");
37    pub const HAS_MIME_TYPE: u64 = q_hash("http://www.w3.org/ns/ma-ont#hasMimeType");
38    pub const HAS_CODEC: u64 = q_hash("http://www.w3.org/ns/ma-ont#hasCodec");
39
40    pub const HAS_BITRATE: u64 = q_hash("http://www.w3.org/ns/ma-ont#hasBitrate");
41    pub const HAS_FRAMERATE: u64 = q_hash("http://www.w3.org/ns/ma-ont#hasFramerate");
42    pub const HAS_SAMPLERATE: u64 = q_hash("http://www.w3.org/ns/ma-ont#hasSamplerate");
43    pub const HAS_CHANNELS: u64 = q_hash("http://www.w3.org/ns/ma-ont#hasChannels");
44
45    /// Qualia extension: packed media time base (ms), separate from Lamport bits.
46    pub const MEDIA_TIME_MS: u64 = q_hash("https://ns.webizen.org/q42/mediaTimeMs");
47}
48
49/// C2PA predicate hashes (`q_hash` of documented IRIs — vocabulary for graph edges only).
50pub mod c2pa {
51    use crate::q_hash;
52
53    pub const HAS_CREDENTIAL: u64 = q_hash("http://ns.c2pa.org/credentials/hasCredential");
54    pub const HAS_MANIFEST: u64 = q_hash("http://ns.c2pa.org/manifest/hasManifest");
55    pub const HAS_SIGNATURE: u64 = q_hash("http://ns.c2pa.org/signature/hasSignature");
56    pub const HAS_PROVENANCE: u64 = q_hash("http://ns.c2pa.org/provenance/hasProvenance");
57    pub const HAS_ASSERTION: u64 = q_hash("http://ns.c2pa.org/assertion/hasAssertion");
58
59    pub const CREATED_AT: u64 = q_hash("http://ns.c2pa.org/provenance/createdAt");
60    pub const CREATED_BY: u64 = q_hash("http://ns.c2pa.org/provenance/createdBy");
61    pub const MODIFIED_AT: u64 = q_hash("http://ns.c2pa.org/provenance/modifiedAt");
62    pub const MODIFIED_BY: u64 = q_hash("http://ns.c2pa.org/provenance/modifiedBy");
63    pub const HAS_TOOL: u64 = q_hash("http://ns.c2pa.org/provenance/hasTool");
64
65    pub const DERIVED_FROM: u64 = q_hash("http://ns.c2pa.org/asset/derivedFrom");
66    pub const COMPONENT_OF: u64 = q_hash("http://ns.c2pa.org/asset/componentOf");
67    pub const HAS_COMPONENT: u64 = q_hash("http://ns.c2pa.org/asset/hasComponent");
68
69    pub const IS_VERIFIED: u64 = q_hash("http://ns.c2pa.org/validation/isVerified");
70    pub const VERIFICATION_STATUS: u64 = q_hash("http://ns.c2pa.org/validation/verificationStatus");
71    pub const HAS_CERTIFICATE: u64 = q_hash("http://ns.c2pa.org/validation/hasCertificate");
72}
73
74/// Honest C2PA verification status (design §6.3.7).
75/// Field presence alone is never "verified".
76#[repr(u8)]
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum C2paVerificationStatus {
79    /// No C2PA path implemented for this asset.
80    Unsupported = 0,
81    /// Manifest/claim edges present; no crypto check run.
82    ParsedOnly = 1,
83    /// Integrity hash matched; signature not checked.
84    IntegrityChecked = 2,
85    /// Signature verified against key material (not yet implemented here).
86    SignatureVerified = 3,
87    /// Full trust chain evaluated (not yet implemented here).
88    TrustChainEvaluated = 4,
89}
90
91/// Media fragment dimensions
92#[repr(C)]
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum MediaFragmentDimension {
95    Temporal {
96        start: u64,
97        end: u64,
98    },
99    Spatial {
100        x: u32,
101        y: u32,
102        width: u32,
103        height: u32,
104    },
105    Track {
106        track_id: u64,
107        track_number: u32,
108    },
109}
110
111/// Media fragment
112#[repr(C)]
113#[derive(Debug, Clone, Copy)]
114pub struct MediaFragment {
115    pub media_uri: u64,
116    pub dimensions: [Option<MediaFragmentDimension>; 4],
117    pub dimension_count: u8,
118}
119
120/// Time window type
121#[repr(C)]
122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub enum WindowType {
124    Tumbling { size_ms: u64 },
125    // NOTE: this WindowType is a SPARQL-MM media-fragment window, NOT a
126    // continuous-query (RSP-QL/C-SPARQL) stream window over the graph. Streaming
127    // SPARQL is planned but unimplemented — see
128    // docs/plans/immersive-sparql-hypermedia-profile.md §15d.
129    Sliding { size_ms: u64, slide_ms: u64 },
130    Session { gap_ms: u64 },
131}
132
133/// Time window
134#[repr(C)]
135#[derive(Debug, Clone, Copy)]
136pub struct TimeWindow {
137    pub window_type: WindowType,
138    pub start_ms: u64,
139    pub end_ms: u64,
140}
141
142/// SPARQL-MM Media Handler
143pub struct SparqlMmHandler<'a> {
144    pub quins: &'a [NQuin],
145    pub windows: [TimeWindow; 64],
146    pub window_count: u8,
147    pub media_fragments: [MediaFragment; 128],
148    pub fragment_count: u8,
149}
150
151impl<'a> SparqlMmHandler<'a> {
152    pub fn new(quins: &'a [NQuin]) -> Self {
153        Self {
154            quins,
155            windows: [TimeWindow {
156                window_type: WindowType::Tumbling { size_ms: 1000 },
157                start_ms: 0,
158                end_ms: 0,
159            }; 64],
160            window_count: 0,
161            media_fragments: [MediaFragment {
162                media_uri: 0,
163                dimensions: [None; 4],
164                dimension_count: 0,
165            }; 128],
166            fragment_count: 0,
167        }
168    }
169
170    /// Build a media fragment from **explicit** dimensions (no hash-derived pseudo-parse).
171    ///
172    /// Callers that only have a media URI hash must pass dimensions separately —
173    /// inventing temporal/spatial ranges from hash bits is forbidden (design §6.3.6).
174    pub fn make_media_fragment(
175        media_uri: u64,
176        dimensions: &[MediaFragmentDimension],
177    ) -> Result<MediaFragment, String> {
178        if dimensions.len() > 4 {
179            return Err("At most 4 fragment dimensions".to_string());
180        }
181        let mut fragment = MediaFragment {
182            media_uri,
183            dimensions: [None; 4],
184            dimension_count: 0,
185        };
186        for (i, d) in dimensions.iter().enumerate() {
187            fragment.dimensions[i] = Some(*d);
188            fragment.dimension_count += 1;
189        }
190        Ok(fragment)
191    }
192
193    /// Legacy entry: returns a fragment with **media_uri only** (no invented dimensions).
194    /// Prefer `make_media_fragment` with explicit temporal/spatial/track dims.
195    pub fn parse_media_fragment(&mut self, fragment_uri: u64) -> Result<MediaFragment, String> {
196        Self::make_media_fragment(fragment_uri, &[])
197    }
198
199    /// Pack xywh into a single object payload (same layout as vision `pack_bbox` family).
200    #[inline]
201    pub fn pack_spatial_u64(x: u32, y: u32, width: u32, height: u32) -> u64 {
202        // Clamp to u16 lanes for fixed packing (normalized or pixel coords ≤ 65535).
203        let x = (x.min(0xFFFF)) as u64;
204        let y = (y.min(0xFFFF)) as u64;
205        let w = (width.min(0xFFFF)) as u64;
206        let h = (height.min(0xFFFF)) as u64;
207        x | (y << 16) | (w << 32) | (h << 48)
208    }
209
210    #[inline]
211    pub fn unpack_spatial_u64(v: u64) -> (u32, u32, u32, u32) {
212        (
213            (v & 0xFFFF) as u32,
214            ((v >> 16) & 0xFFFF) as u32,
215            ((v >> 32) & 0xFFFF) as u32,
216            ((v >> 48) & 0xFFFF) as u32,
217        )
218    }
219
220    /// Axis-aligned box intersection (x,y,w,h).
221    #[inline]
222    pub fn spatial_intersects(
223        ax: u32,
224        ay: u32,
225        aw: u32,
226        ah: u32,
227        bx: u32,
228        by: u32,
229        bw: u32,
230        bh: u32,
231    ) -> bool {
232        let ax1 = ax.saturating_add(aw);
233        let ay1 = ay.saturating_add(ah);
234        let bx1 = bx.saturating_add(bw);
235        let by1 = by.saturating_add(bh);
236        ax < bx1 && ax1 > bx && ay < by1 && ay1 > by
237    }
238
239    /// Media time from a quin that uses `ma_ont::MEDIA_TIME_MS` (object = ms),
240    /// **not** the Lamport field in metadata.
241    pub fn media_time_ms(quin: &NQuin) -> Option<u64> {
242        if quin.predicate == ma_ont::MEDIA_TIME_MS {
243            Some(quin.object)
244        } else {
245            None
246        }
247    }
248
249    /// Get MA Ontology property for a media resource
250    pub fn get_ma_property(&self, media_uri: u64, predicate: u64) -> Result<u64, String> {
251        for quin in self.quins {
252            if quin.subject == media_uri && quin.predicate == predicate {
253                return Ok(quin.object);
254            }
255        }
256        Err("Property not found".to_string())
257    }
258
259    /// Get temporal fragment using MA Ontology
260    pub fn get_temporal_fragment(&self, media_uri: u64) -> Result<(u64, u64), String> {
261        let start = self.get_ma_property(media_uri, ma_ont::HAS_START_TIME)?;
262        let end = self.get_ma_property(media_uri, ma_ont::HAS_END_TIME)?;
263        Ok((start, end))
264    }
265
266    /// Get spatial fragment using MA Ontology
267    pub fn get_spatial_fragment(&self, media_uri: u64) -> Result<(u32, u32, u32, u32), String> {
268        let x = self.get_ma_property(media_uri, ma_ont::HAS_X)? as u32;
269        let y = self.get_ma_property(media_uri, ma_ont::HAS_Y)? as u32;
270        let width = self.get_ma_property(media_uri, ma_ont::HAS_WIDTH)? as u32;
271        let height = self.get_ma_property(media_uri, ma_ont::HAS_HEIGHT)? as u32;
272        Ok((x, y, width, height))
273    }
274
275    /// Get track fragment using MA Ontology
276    pub fn get_track_fragment(&self, media_uri: u64) -> Result<(u64, u32), String> {
277        let track_id = self.get_ma_property(media_uri, ma_ont::HAS_TRACK)?;
278        let track_number = self.get_ma_property(media_uri, ma_ont::HAS_TRACK_NUMBER)? as u32;
279        Ok((track_id, track_number))
280    }
281
282    /// Add a media fragment
283    pub fn add_media_fragment(&mut self, fragment: MediaFragment) -> Result<u8, String> {
284        if self.fragment_count >= 128 {
285            return Err("Fragment overflow".to_string());
286        }
287        let idx = self.fragment_count;
288        self.media_fragments[idx as usize] = fragment;
289        self.fragment_count += 1;
290        Ok(idx)
291    }
292
293    /// Create a tumbling time window
294    pub fn create_tumbling_window(&mut self, size_ms: u64, start_ms: u64) -> Result<u8, String> {
295        if self.window_count >= 64 {
296            return Err("Window overflow".to_string());
297        }
298        let idx = self.window_count;
299        self.windows[idx as usize] = TimeWindow {
300            window_type: WindowType::Tumbling { size_ms },
301            start_ms,
302            end_ms: start_ms + size_ms,
303        };
304        self.window_count += 1;
305        Ok(idx)
306    }
307
308    /// Create a sliding time window
309    pub fn create_sliding_window(
310        &mut self,
311        size_ms: u64,
312        slide_ms: u64,
313        start_ms: u64,
314    ) -> Result<u8, String> {
315        if self.window_count >= 64 {
316            return Err("Window overflow".to_string());
317        }
318        let idx = self.window_count;
319        self.windows[idx as usize] = TimeWindow {
320            window_type: WindowType::Sliding { size_ms, slide_ms },
321            start_ms,
322            end_ms: start_ms + size_ms,
323        };
324        self.window_count += 1;
325        Ok(idx)
326    }
327
328    /// Create a session window
329    pub fn create_session_window(&mut self, gap_ms: u64, start_ms: u64) -> Result<u8, String> {
330        if self.window_count >= 64 {
331            return Err("Window overflow".to_string());
332        }
333        let idx = self.window_count;
334        self.windows[idx as usize] = TimeWindow {
335            window_type: WindowType::Session { gap_ms },
336            start_ms,
337            end_ms: 0, // Dynamic
338        };
339        self.window_count += 1;
340        Ok(idx)
341    }
342
343    /// Query quins within a time window (heap-compatible wrapper).
344    /// Prefer `query_window_into` on hot paths.
345    pub fn query_window(
346        &self,
347        window_id: u8,
348        _timestamp_field: u64,
349    ) -> Result<Vec<&NQuin>, String> {
350        let mut buf = [None; 256];
351        let n = self.query_window_into(window_id, &mut buf)?;
352        Ok(buf[..n].iter().filter_map(|x| *x).collect())
353    }
354
355    /// Caller-buffered window query. Uses **media time** when
356    /// `predicate == MEDIA_TIME_MS`; otherwise falls back to metadata low 29 bits
357    /// with the understanding that that path is Lamport-mixed (not pure media time).
358    pub fn query_window_into(
359        &'a self,
360        window_id: u8,
361        out: &mut [Option<&'a NQuin>],
362    ) -> Result<usize, String> {
363        let window = self
364            .windows
365            .get(window_id as usize)
366            .filter(|_| (window_id as usize) < self.window_count as usize)
367            .ok_or("Window ID out of bounds")?;
368
369        let mut w = 0usize;
370        for quin in self.quins {
371            let t = if let Some(ms) = Self::media_time_ms(quin) {
372                ms
373            } else {
374                quin.metadata & 0x1FFF_FFFF
375            };
376            if t >= window.start_ms && t <= window.end_ms {
377                if w >= out.len() {
378                    break;
379                }
380                out[w] = Some(quin);
381                w += 1;
382            }
383        }
384        Ok(w)
385    }
386
387    /// Query media fragment (heap wrapper). Prefer `query_media_fragment_into`.
388    pub fn query_media_fragment(&self, fragment_id: u8) -> Result<Vec<&NQuin>, String> {
389        let mut buf = [None; 256];
390        let n = self.query_media_fragment_into(fragment_id, &mut buf)?;
391        Ok(buf[..n].iter().filter_map(|x| *x).collect())
392    }
393
394    /// Caller-buffered media-fragment query with real spatial intersection.
395    pub fn query_media_fragment_into(
396        &'a self,
397        fragment_id: u8,
398        out: &mut [Option<&'a NQuin>],
399    ) -> Result<usize, String> {
400        let fragment = self
401            .media_fragments
402            .get(fragment_id as usize)
403            .filter(|_| (fragment_id as usize) < self.fragment_count as usize)
404            .ok_or("Fragment ID out of bounds")?;
405
406        let mut w = 0usize;
407        for quin in self.quins {
408            if !self.check_fragment_match(quin, fragment) {
409                continue;
410            }
411            if w >= out.len() {
412                break;
413            }
414            out[w] = Some(quin);
415            w += 1;
416        }
417        Ok(w)
418    }
419
420    fn check_fragment_match(&self, quin: &NQuin, fragment: &MediaFragment) -> bool {
421        if fragment.dimension_count == 0 {
422            return quin.subject == fragment.media_uri
423                || self.quin_linked_to_media(quin, fragment.media_uri);
424        }
425
426        if !(quin.subject == fragment.media_uri
427            || self.quin_linked_to_media(quin, fragment.media_uri))
428        {
429            return false;
430        }
431
432        for i in 0..fragment.dimension_count as usize {
433            let Some(dim) = fragment.dimensions[i] else {
434                continue;
435            };
436            match dim {
437                MediaFragmentDimension::Temporal { start, end } => {
438                    let Some(quin_time) = Self::media_time_ms(quin).or_else(|| {
439                        if quin.predicate == ma_ont::HAS_START_TIME
440                            || quin.predicate == ma_ont::HAS_END_TIME
441                        {
442                            Some(quin.object)
443                        } else {
444                            None
445                        }
446                    }) else {
447                        // No media-time payload on this quin → temporal dim does not reject.
448                        continue;
449                    };
450                    if quin_time < start || quin_time > end {
451                        return false;
452                    }
453                }
454                MediaFragmentDimension::Spatial {
455                    x,
456                    y,
457                    width,
458                    height,
459                } => {
460                    // If this quin carries a box, require intersection; else pass.
461                    if let Some((qx, qy, qw, qh)) = self.quin_spatial_box(quin) {
462                        if !Self::spatial_intersects(qx, qy, qw, qh, x, y, width, height) {
463                            return false;
464                        }
465                    }
466                }
467                MediaFragmentDimension::Track { track_id, .. } => {
468                    let is_track_pred = quin.predicate == ma_ont::HAS_TRACK
469                        || quin.predicate == ma_ont::HAS_TRACK_NUMBER
470                        || quin.predicate == q_hash("https://ns.webizen.org/q42/hasTrackId");
471                    if is_track_pred && quin.object != track_id {
472                        return false;
473                    }
474                }
475            }
476        }
477        true
478    }
479
480    fn quin_linked_to_media(&self, quin: &NQuin, media_uri: u64) -> bool {
481        if quin.subject == media_uri {
482            return true;
483        }
484        // Observation pattern: media --VisualObservation--> instance
485        for q in self.quins {
486            if q.subject == media_uri && q.object == quin.subject {
487                return true;
488            }
489        }
490        false
491    }
492
493    /// Resolve a box for this quin: packed HAS_SPATIAL_FRAGMENT object, or HAS_X/Y/W/H props.
494    fn quin_spatial_box(&self, quin: &NQuin) -> Option<(u32, u32, u32, u32)> {
495        if quin.predicate == ma_ont::HAS_SPATIAL_FRAGMENT
496            || quin.predicate == q_hash("https://ns.webizen.org/q42/hasBoundingBox")
497        {
498            let (x, y, w, h) = Self::unpack_spatial_u64(quin.object);
499            // hasBoundingBox stores x0,y0,x1,y1 not x,y,w,h — convert if x1>x0 style.
500            if quin.predicate == q_hash("https://ns.webizen.org/q42/hasBoundingBox") {
501                let x1 = w;
502                let y1 = h;
503                let width = x1.saturating_sub(x);
504                let height = y1.saturating_sub(y);
505                return Some((x, y, width, height));
506            }
507            return Some((x, y, w, h));
508        }
509        // Compose from component properties on same subject.
510        let mut x = None;
511        let mut y = None;
512        let mut width = None;
513        let mut height = None;
514        for q in self.quins {
515            if q.subject != quin.subject {
516                continue;
517            }
518            match q.predicate {
519                p if p == ma_ont::HAS_X => x = Some(q.object as u32),
520                p if p == ma_ont::HAS_Y => y = Some(q.object as u32),
521                p if p == ma_ont::HAS_WIDTH => width = Some(q.object as u32),
522                p if p == ma_ont::HAS_HEIGHT => height = Some(q.object as u32),
523                _ => {}
524            }
525        }
526        match (x, y, width, height) {
527            (Some(x), Some(y), Some(w), Some(h)) => Some((x, y, w, h)),
528            _ => None,
529        }
530    }
531
532    /// Get media duration using MA Ontology
533    pub fn get_media_duration(&self, media_uri: u64) -> Result<u64, String> {
534        self.get_ma_property(media_uri, ma_ont::DURATION)
535    }
536
537    /// Get media dimensions using MA Ontology
538    pub fn get_media_dimensions(&self, media_uri: u64) -> Result<(u32, u32), String> {
539        let width = self.get_ma_property(media_uri, ma_ont::HAS_WIDTH)? as u32;
540        let height = self.get_ma_property(media_uri, ma_ont::HAS_HEIGHT)? as u32;
541        Ok((width, height))
542    }
543
544    /// Get media format using MA Ontology
545    pub fn get_media_format(&self, media_uri: u64) -> Result<u64, String> {
546        self.get_ma_property(media_uri, ma_ont::HAS_FORMAT)
547    }
548
549    /// Get media MIME type using MA Ontology
550    pub fn get_media_mime_type(&self, media_uri: u64) -> Result<u64, String> {
551        self.get_ma_property(media_uri, ma_ont::HAS_MIME_TYPE)
552    }
553
554    /// Get media codec using MA Ontology
555    pub fn get_media_codec(&self, media_uri: u64) -> Result<u64, String> {
556        self.get_ma_property(media_uri, ma_ont::HAS_CODEC)
557    }
558
559    /// Get media bitrate using MA Ontology
560    pub fn get_media_bitrate(&self, media_uri: u64) -> Result<u64, String> {
561        self.get_ma_property(media_uri, ma_ont::HAS_BITRATE)
562    }
563
564    /// Get media framerate using MA Ontology
565    pub fn get_media_framerate(&self, media_uri: u64) -> Result<u64, String> {
566        self.get_ma_property(media_uri, ma_ont::HAS_FRAMERATE)
567    }
568
569    /// C2PA: Get content credential for media
570    pub fn get_credential(&self, media_uri: u64) -> Result<u64, String> {
571        self.get_ma_property(media_uri, c2pa::HAS_CREDENTIAL)
572    }
573
574    /// C2PA: Get manifest for media
575    pub fn get_manifest(&self, media_uri: u64) -> Result<u64, String> {
576        self.get_ma_property(media_uri, c2pa::HAS_MANIFEST)
577    }
578
579    /// C2PA: Get signature for media
580    pub fn get_signature(&self, media_uri: u64) -> Result<u64, String> {
581        self.get_ma_property(media_uri, c2pa::HAS_SIGNATURE)
582    }
583
584    /// C2PA: Get provenance for media
585    pub fn get_provenance(&self, media_uri: u64) -> Result<u64, String> {
586        self.get_ma_property(media_uri, c2pa::HAS_PROVENANCE)
587    }
588
589    /// C2PA: Check if media is cryptographically verified.
590    ///
591    /// Honest policy: a stored `isVerified=1` edge is **not** sufficient.
592    /// Returns `Ok(true)` only when status is SignatureVerified or TrustChainEvaluated.
593    /// Today this engine never reaches those levels → typically `Ok(false)` or Err.
594    pub fn is_verified(&self, media_uri: u64) -> Result<bool, String> {
595        let status = self.c2pa_status(media_uri)?;
596        Ok(matches!(
597            status,
598            C2paVerificationStatus::SignatureVerified | C2paVerificationStatus::TrustChainEvaluated
599        ))
600    }
601
602    /// Honest verification ladder for C2PA (design §6.3.7).
603    pub fn c2pa_status(&self, media_uri: u64) -> Result<C2paVerificationStatus, String> {
604        // Full crypto path is not implemented in this module.
605        let has_manifest = self.get_ma_property(media_uri, c2pa::HAS_MANIFEST).is_ok();
606        let has_sig = self.get_ma_property(media_uri, c2pa::HAS_SIGNATURE).is_ok();
607        if !has_manifest && !has_sig {
608            return Ok(C2paVerificationStatus::Unsupported);
609        }
610        // Edges present only → ParsedOnly. Never promote to verified.
611        let _claimed = self.get_ma_property(media_uri, c2pa::IS_VERIFIED);
612        Ok(C2paVerificationStatus::ParsedOnly)
613    }
614
615    /// C2PA: Get verification status as u64 enum discriminant.
616    pub fn get_verification_status(&self, media_uri: u64) -> Result<u64, String> {
617        Ok(self.c2pa_status(media_uri)? as u64)
618    }
619
620    /// C2PA: Get creation timestamp
621    pub fn get_created_at(&self, media_uri: u64) -> Result<u64, String> {
622        self.get_ma_property(media_uri, c2pa::CREATED_AT)
623    }
624
625    /// C2PA: Get creator
626    pub fn get_created_by(&self, media_uri: u64) -> Result<u64, String> {
627        self.get_ma_property(media_uri, c2pa::CREATED_BY)
628    }
629
630    /// C2PA: Get modification timestamp
631    pub fn get_modified_at(&self, media_uri: u64) -> Result<u64, String> {
632        self.get_ma_property(media_uri, c2pa::MODIFIED_AT)
633    }
634
635    /// C2PA: Get modifier
636    pub fn get_modified_by(&self, media_uri: u64) -> Result<u64, String> {
637        self.get_ma_property(media_uri, c2pa::MODIFIED_BY)
638    }
639
640    /// C2PA: Get tool used to create media
641    pub fn get_tool(&self, media_uri: u64) -> Result<u64, String> {
642        self.get_ma_property(media_uri, c2pa::HAS_TOOL)
643    }
644
645    /// C2PA: Get source asset (derived from)
646    pub fn get_derived_from(&self, media_uri: u64) -> Result<u64, String> {
647        self.get_ma_property(media_uri, c2pa::DERIVED_FROM)
648    }
649
650    /// C2PA: Get parent asset (component of)
651    pub fn get_component_of(&self, media_uri: u64) -> Result<u64, String> {
652        self.get_ma_property(media_uri, c2pa::COMPONENT_OF)
653    }
654
655    /// C2PA: Get component assets
656    pub fn get_components(&self, media_uri: u64) -> Result<Vec<u64>, String> {
657        let mut components = Vec::new();
658        for quin in self.quins {
659            if quin.subject == media_uri && quin.predicate == c2pa::HAS_COMPONENT {
660                components.push(quin.object);
661            }
662        }
663        Ok(components)
664    }
665
666    /// C2PA: Verify content signature.
667    /// **Unsupported** in this build — always returns `Ok(false)` if a signature
668    /// edge exists (ParsedOnly), or `Err` if missing. Never claims crypto success.
669    pub fn verify_signature(&self, media_uri: u64) -> Result<bool, String> {
670        let _signature = self.get_signature(media_uri)?;
671        // Real signature verification is out of scope for SPARQL-MM accessors.
672        Ok(false)
673    }
674
675    /// Aggregate over time window
676    pub fn window_aggregate(
677        &self,
678        window_id: u8,
679        aggregate_fn: fn(&[&NQuin]) -> u64,
680    ) -> Result<u64, String> {
681        let quins = self.query_window(window_id, 0)?;
682        Ok(aggregate_fn(&quins))
683    }
684}
685
686impl<'a> Default for SparqlMmHandler<'a> {
687    fn default() -> Self {
688        Self::new(&[])
689    }
690}
691
692/// SPARQL-MM extension functions
693pub fn mm_duration(args: &[u64], quins: &[NQuin], result: &mut BindingRow) -> bool {
694    if args.is_empty() {
695        return false;
696    }
697    let media_uri = args[0];
698
699    let handler = SparqlMmHandler::new(quins);
700    match handler.get_media_duration(media_uri) {
701        Ok(duration) => {
702            result.slots[0] = Some(duration);
703            true
704        }
705        Err(_) => false,
706    }
707}
708
709pub fn mm_dimensions(args: &[u64], quins: &[NQuin], result: &mut BindingRow) -> bool {
710    if args.is_empty() {
711        return false;
712    }
713    let media_uri = args[0];
714
715    let handler = SparqlMmHandler::new(quins);
716    match handler.get_media_dimensions(media_uri) {
717        Ok((width, height)) => {
718            result.slots[0] = Some(width as u64);
719            result.slots[1] = Some(height as u64);
720            true
721        }
722        Err(_) => false,
723    }
724}
725
726pub fn mm_temporal_fragment(args: &[u64], quins: &[NQuin], result: &mut BindingRow) -> bool {
727    if args.len() < 2 {
728        return false;
729    }
730    let media_uri = args[0];
731    let start = args[1];
732    let end = args.get(2).copied().unwrap_or(start);
733
734    let mut handler = SparqlMmHandler::new(quins);
735    let fragment = MediaFragment {
736        media_uri,
737        dimensions: [
738            Some(MediaFragmentDimension::Temporal { start, end }),
739            None,
740            None,
741            None,
742        ],
743        dimension_count: 1,
744    };
745
746    match handler.add_media_fragment(fragment) {
747        Ok(_) => {
748            result.slots[0] = Some(1); // Success
749            true
750        }
751        Err(_) => false,
752    }
753}
754
755/// MA Ontology extension functions
756pub fn ma_format(args: &[u64], quins: &[NQuin], result: &mut BindingRow) -> bool {
757    if args.is_empty() {
758        return false;
759    }
760    let media_uri = args[0];
761
762    let handler = SparqlMmHandler::new(quins);
763    match handler.get_media_format(media_uri) {
764        Ok(format) => {
765            result.slots[0] = Some(format);
766            true
767        }
768        Err(_) => false,
769    }
770}
771
772pub fn ma_mime_type(args: &[u64], quins: &[NQuin], result: &mut BindingRow) -> bool {
773    if args.is_empty() {
774        return false;
775    }
776    let media_uri = args[0];
777
778    let handler = SparqlMmHandler::new(quins);
779    match handler.get_media_mime_type(media_uri) {
780        Ok(mime_type) => {
781            result.slots[0] = Some(mime_type);
782            true
783        }
784        Err(_) => false,
785    }
786}
787
788pub fn ma_codec(args: &[u64], quins: &[NQuin], result: &mut BindingRow) -> bool {
789    if args.is_empty() {
790        return false;
791    }
792    let media_uri = args[0];
793
794    let handler = SparqlMmHandler::new(quins);
795    match handler.get_media_codec(media_uri) {
796        Ok(codec) => {
797            result.slots[0] = Some(codec);
798            true
799        }
800        Err(_) => false,
801    }
802}
803
804pub fn ma_bitrate(args: &[u64], quins: &[NQuin], result: &mut BindingRow) -> bool {
805    if args.is_empty() {
806        return false;
807    }
808    let media_uri = args[0];
809
810    let handler = SparqlMmHandler::new(quins);
811    match handler.get_media_bitrate(media_uri) {
812        Ok(bitrate) => {
813            result.slots[0] = Some(bitrate);
814            true
815        }
816        Err(_) => false,
817    }
818}
819pub fn ma_framerate(args: &[u64], quins: &[NQuin], result: &mut BindingRow) -> bool {
820    if args.is_empty() {
821        return false;
822    }
823    let media_uri = args[0];
824
825    let handler = SparqlMmHandler::new(quins);
826    match handler.get_media_framerate(media_uri) {
827        Ok(framerate) => {
828            result.slots[0] = Some(framerate);
829            true
830        }
831        Err(_) => false,
832    }
833}
834
835/// C2PA extension functions
836
837/// c2pa:credential - get content credential
838pub fn c2pa_credential(args: &[u64], quins: &[NQuin], result: &mut BindingRow) -> bool {
839    if args.is_empty() {
840        return false;
841    }
842    let media_uri = args[0];
843
844    let handler = SparqlMmHandler::new(quins);
845    match handler.get_credential(media_uri) {
846        Ok(credential) => {
847            result.slots[0] = Some(credential);
848            true
849        }
850        Err(_) => false,
851    }
852}
853
854/// c2pa:isVerified - check if media is verified
855pub fn c2pa_is_verified(args: &[u64], quins: &[NQuin], result: &mut BindingRow) -> bool {
856    if args.is_empty() {
857        return false;
858    }
859    let media_uri = args[0];
860
861    let handler = SparqlMmHandler::new(quins);
862    match handler.is_verified(media_uri) {
863        Ok(verified) => {
864            result.slots[0] = Some(if verified { 1 } else { 0 });
865            true
866        }
867        Err(_) => false,
868    }
869}
870
871/// c2pa:verificationStatus - get verification status
872pub fn c2pa_verification_status(args: &[u64], quins: &[NQuin], result: &mut BindingRow) -> bool {
873    if args.is_empty() {
874        return false;
875    }
876    let media_uri = args[0];
877
878    let handler = SparqlMmHandler::new(quins);
879    match handler.get_verification_status(media_uri) {
880        Ok(status) => {
881            result.slots[0] = Some(status);
882            true
883        }
884        Err(_) => false,
885    }
886}
887
888/// c2pa:createdAt - get creation timestamp
889pub fn c2pa_created_at(args: &[u64], quins: &[NQuin], result: &mut BindingRow) -> bool {
890    if args.is_empty() {
891        return false;
892    }
893    let media_uri = args[0];
894
895    let handler = SparqlMmHandler::new(quins);
896    match handler.get_created_at(media_uri) {
897        Ok(timestamp) => {
898            result.slots[0] = Some(timestamp);
899            true
900        }
901        Err(_) => false,
902    }
903}
904
905/// c2pa:createdBy - get creator
906pub fn c2pa_created_by(args: &[u64], quins: &[NQuin], result: &mut BindingRow) -> bool {
907    if args.is_empty() {
908        return false;
909    }
910    let media_uri = args[0];
911
912    let handler = SparqlMmHandler::new(quins);
913    match handler.get_created_by(media_uri) {
914        Ok(creator) => {
915            result.slots[0] = Some(creator);
916            true
917        }
918        Err(_) => false,
919    }
920}
921
922/// c2pa:verifySignature - verify content signature
923pub fn c2pa_verify_signature(args: &[u64], quins: &[NQuin], result: &mut BindingRow) -> bool {
924    if args.is_empty() {
925        return false;
926    }
927    let media_uri = args[0];
928
929    let handler = SparqlMmHandler::new(quins);
930    match handler.verify_signature(media_uri) {
931        Ok(verified) => {
932            result.slots[0] = Some(if verified { 1 } else { 0 });
933            true
934        }
935        Err(_) => false,
936    }
937}
938
939/// c2pa:derivedFrom - get source asset
940pub fn c2pa_derived_from(args: &[u64], quins: &[NQuin], result: &mut BindingRow) -> bool {
941    if args.is_empty() {
942        return false;
943    }
944    let media_uri = args[0];
945
946    let handler = SparqlMmHandler::new(quins);
947    match handler.get_derived_from(media_uri) {
948        Ok(source) => {
949            result.slots[0] = Some(source);
950            true
951        }
952        Err(_) => false,
953    }
954}
955
956#[cfg(test)]
957mod tests {
958    use super::*;
959
960    #[test]
961    fn test_mm_handler_creation() {
962        let quins = vec![];
963        let handler = SparqlMmHandler::new(&quins);
964        assert_eq!(handler.window_count, 0);
965    }
966
967    #[test]
968    fn test_create_tumbling_window() {
969        let quins = vec![];
970        let mut handler = SparqlMmHandler::new(&quins);
971
972        let result = handler.create_tumbling_window(1000, 0);
973        assert!(result.is_ok());
974        assert_eq!(handler.window_count, 1);
975    }
976
977    #[test]
978    fn test_create_sliding_window() {
979        let quins = vec![];
980        let mut handler = SparqlMmHandler::new(&quins);
981
982        let result = handler.create_sliding_window(1000, 500, 0);
983        assert!(result.is_ok());
984        assert_eq!(handler.window_count, 1);
985    }
986
987    #[test]
988    fn test_parse_media_fragment_no_invented_dims() {
989        let quins = vec![];
990        let mut handler = SparqlMmHandler::new(&quins);
991
992        let fragment = handler.parse_media_fragment(12345).unwrap();
993        assert_eq!(fragment.media_uri, 12345);
994        assert_eq!(fragment.dimension_count, 0);
995    }
996
997    #[test]
998    fn ma_ont_constants_are_distinct() {
999        // Former placeholders collided (HAS_FRAGMENT == HAS_CODEC etc.).
1000        assert_ne!(ma_ont::HAS_FRAGMENT, ma_ont::HAS_CODEC);
1001        assert_ne!(ma_ont::HAS_BITRATE, ma_ont::HAS_TEMPORAL_FRAGMENT);
1002        assert_ne!(ma_ont::HAS_X, ma_ont::HAS_Y);
1003        assert_ne!(c2pa::HAS_CREDENTIAL, c2pa::HAS_MANIFEST);
1004        assert_ne!(c2pa::DERIVED_FROM, ma_ont::HAS_MIME_TYPE);
1005    }
1006
1007    #[test]
1008    fn spatial_intersection_real() {
1009        assert!(SparqlMmHandler::spatial_intersects(
1010            0, 0, 10, 10, 5, 5, 10, 10
1011        ));
1012        assert!(!SparqlMmHandler::spatial_intersects(
1013            0, 0, 10, 10, 20, 20, 5, 5
1014        ));
1015    }
1016
1017    #[test]
1018    fn query_window_into_media_time() {
1019        let media = q_hash("media:clip");
1020        let quins = [
1021            NQuin {
1022                subject: media,
1023                predicate: ma_ont::MEDIA_TIME_MS,
1024                object: 500,
1025                context: 0,
1026                metadata: 0,
1027                parity: 0,
1028            },
1029            NQuin {
1030                subject: media,
1031                predicate: ma_ont::MEDIA_TIME_MS,
1032                object: 2500,
1033                context: 0,
1034                metadata: 0,
1035                parity: 0,
1036            },
1037        ];
1038        // Fix parity for honesty (not required for query)
1039        let mut handler = SparqlMmHandler::new(&quins);
1040        handler.create_tumbling_window(1000, 0).unwrap(); // [0,1000]
1041        let mut out = [None; 8];
1042        let n = handler.query_window_into(0, &mut out).unwrap();
1043        assert_eq!(n, 1);
1044        assert_eq!(out[0].unwrap().object, 500);
1045    }
1046
1047    #[test]
1048    fn query_media_fragment_into_spatial() {
1049        let media = q_hash("media:img");
1050        let packed = SparqlMmHandler::pack_spatial_u64(10, 10, 50, 50);
1051        let quins = [NQuin {
1052            subject: media,
1053            predicate: ma_ont::HAS_SPATIAL_FRAGMENT,
1054            object: packed,
1055            context: 0,
1056            metadata: 0,
1057            parity: 0,
1058        }];
1059        let mut handler = SparqlMmHandler::new(&quins);
1060        let frag = SparqlMmHandler::make_media_fragment(
1061            media,
1062            &[MediaFragmentDimension::Spatial {
1063                x: 20,
1064                y: 20,
1065                width: 20,
1066                height: 20,
1067            }],
1068        )
1069        .unwrap();
1070        handler.add_media_fragment(frag).unwrap();
1071        let mut out = [None; 4];
1072        let n = handler.query_media_fragment_into(0, &mut out).unwrap();
1073        assert_eq!(n, 1);
1074    }
1075
1076    #[test]
1077    fn c2pa_never_claims_verified_from_field_presence() {
1078        let media = q_hash("media:photo");
1079        let quins = [
1080            NQuin {
1081                subject: media,
1082                predicate: c2pa::HAS_SIGNATURE,
1083                object: 0xABC,
1084                context: 0,
1085                metadata: 0,
1086                parity: 0,
1087            },
1088            NQuin {
1089                subject: media,
1090                predicate: c2pa::IS_VERIFIED,
1091                object: 1,
1092                context: 0,
1093                metadata: 0,
1094                parity: 0,
1095            },
1096        ];
1097        let handler = SparqlMmHandler::new(&quins);
1098        assert_eq!(
1099            handler.c2pa_status(media).unwrap(),
1100            C2paVerificationStatus::ParsedOnly
1101        );
1102        assert!(!handler.is_verified(media).unwrap());
1103        assert!(!handler.verify_signature(media).unwrap());
1104    }
1105}