Skip to main content

sim_lib_web_core/
records.rs

1use crate::{EvidenceSelector, PolicyKind, wire::representation_identity};
2use sim_kernel::{ContentId, Datum};
3use sim_lib_net_core::RetrievalUri;
4use std::{error::Error, fmt};
5
6/// Conservative public decode limits for canonical record projections.
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub struct DecodeLimits {
9    pub max_text_bytes: usize,
10    pub max_body_bytes: usize,
11    pub max_items: usize,
12}
13impl Default for DecodeLimits {
14    fn default() -> Self {
15        Self {
16            max_text_bytes: 1_048_576,
17            max_body_bytes: 16_777_216,
18            max_items: 4_096,
19        }
20    }
21}
22
23/// Validation or bounded decoding refusal.
24#[derive(Clone, Debug, PartialEq, Eq)]
25pub enum WebRecordError {
26    BoundExceeded(&'static str),
27    InvalidSelector,
28    ContentIdentity,
29    MissingDecision(PolicyKind),
30    InvalidRecord(&'static str),
31}
32impl fmt::Display for WebRecordError {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        write!(f, "{self:?}")
35    }
36}
37impl Error for WebRecordError {}
38
39/// Raw retrieved bytes with their own content identity.
40#[derive(Clone, Debug, PartialEq, Eq)]
41pub struct WebCapture {
42    pub retrieval_uri: RetrievalUri,
43    pub content_id: ContentId,
44    pub body: Vec<u8>,
45    pub exchange: WebExchange,
46}
47impl WebCapture {
48    pub fn checked(
49        retrieval_uri: RetrievalUri,
50        content_id: ContentId,
51        body: Vec<u8>,
52        exchange: WebExchange,
53        limits: DecodeLimits,
54    ) -> Result<Self, WebRecordError> {
55        if body.len() > limits.max_body_bytes {
56            return Err(WebRecordError::BoundExceeded("raw body"));
57        }
58        let actual = Datum::Bytes(body.clone())
59            .content_id()
60            .map_err(|_| WebRecordError::ContentIdentity)?;
61        if actual != content_id {
62            return Err(WebRecordError::ContentIdentity);
63        }
64        Ok(Self {
65            retrieval_uri,
66            content_id,
67            body,
68            exchange,
69        })
70    }
71}
72
73/// Provider-neutral request/response exchange facts.
74#[derive(Clone, Debug, PartialEq, Eq)]
75pub struct WebExchange {
76    pub method: String,
77    pub status: u16,
78    pub final_uri: String,
79    pub media_type: Option<String>,
80    pub received_bytes: u64,
81}
82
83/// Immutable normalized text and the provenance required to interpret it.
84#[derive(Clone, Debug, PartialEq, Eq)]
85pub struct WebRepresentation {
86    pub content_id: ContentId,
87    pub raw_source_id: ContentId,
88    pub text: String,
89    pub codec: String,
90    pub codec_version: String,
91    pub media_type: String,
92    pub charset: Option<String>,
93    pub language: Option<String>,
94    pub fidelity_warnings: Vec<String>,
95}
96/// Codec and fidelity provenance for one normalized representation.
97#[derive(Clone, Debug, PartialEq, Eq)]
98pub struct RepresentationMetadata {
99    pub codec: String,
100    pub codec_version: String,
101    pub media_type: String,
102    pub charset: Option<String>,
103    pub language: Option<String>,
104    pub fidelity_warnings: Vec<String>,
105}
106impl WebRepresentation {
107    pub fn checked(
108        raw_source_id: ContentId,
109        text: String,
110        metadata: RepresentationMetadata,
111        limits: DecodeLimits,
112    ) -> Result<Self, WebRecordError> {
113        if text.len() > limits.max_text_bytes || metadata.fidelity_warnings.len() > limits.max_items
114        {
115            return Err(WebRecordError::BoundExceeded("representation"));
116        }
117        let identity = representation_identity(&raw_source_id, &text, &metadata);
118        let content_id = identity
119            .content_id()
120            .map_err(|_| WebRecordError::ContentIdentity)?;
121        if content_id == raw_source_id {
122            return Err(WebRecordError::ContentIdentity);
123        }
124        Ok(Self {
125            content_id,
126            raw_source_id,
127            text,
128            codec: metadata.codec,
129            codec_version: metadata.codec_version,
130            media_type: metadata.media_type,
131            charset: metadata.charset,
132            language: metadata.language,
133            fidelity_warnings: metadata.fidelity_warnings,
134        })
135    }
136    /// Select Unicode scalar offsets from this immutable representation.
137    pub fn select(&self, start: u32, end: u32) -> Result<EvidenceSelector, WebRecordError> {
138        let count = self.text.chars().count();
139        if start > end || end as usize > count {
140            return Err(WebRecordError::InvalidSelector);
141        }
142        let exact = self
143            .text
144            .chars()
145            .skip(start as usize)
146            .take((end - start) as usize)
147            .collect();
148        EvidenceSelector::checked(self.content_id.clone(), start, end, exact, &self.text)
149    }
150}