1use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7use crate::annotations::Annotations;
8use crate::document::CadIr;
9use crate::native::NativeConvertError;
10use crate::unknown::UnknownRecord;
11
12pub const SOURCE_FIDELITY_VERSION: &str = "3";
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
17pub struct RetainedSourceRecord {
18 pub id: String,
20 pub stream: String,
22 pub offset: u64,
24 pub byte_len: u64,
26 pub sha256: String,
28 #[serde(
30 default,
31 skip_serializing_if = "Option::is_none",
32 with = "crate::bytes::option"
33 )]
34 #[schemars(with = "Option<String>")]
35 pub data: Option<Vec<u8>>,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
40pub enum FidelityError {
41 #[error("unsupported source-fidelity version: {found}")]
43 Version {
44 found: String,
46 },
47 #[error("duplicate retained source record: {id}")]
49 DuplicateRecord {
50 id: String,
52 },
53 #[error("retained source record {id} declares {declared} bytes but contains {actual}")]
55 Length {
56 id: String,
58 declared: u64,
60 actual: u64,
62 },
63 #[error("retained source record {id} does not match its SHA-256 digest")]
65 Digest {
66 id: String,
68 },
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
73pub struct SourceFidelity {
74 pub version: String,
76 #[serde(default)]
78 pub annotations: Annotations,
79 #[serde(default, skip_serializing_if = "Vec::is_empty")]
81 pub retained_records: Vec<RetainedSourceRecord>,
82}
83
84impl Default for SourceFidelity {
85 fn default() -> Self {
86 Self {
87 version: SOURCE_FIDELITY_VERSION.into(),
88 annotations: Annotations::default(),
89 retained_records: Vec::new(),
90 }
91 }
92}
93
94impl SourceFidelity {
95 pub fn finalize(&mut self) {
97 self.retained_records.sort_by(|left, right| {
98 (&left.stream, left.offset, &left.id).cmp(&(&right.stream, right.offset, &right.id))
99 });
100 }
101
102 pub fn retained_record(&self, id: &str) -> Option<&RetainedSourceRecord> {
104 self.retained_records.iter().find(|record| record.id == id)
105 }
106
107 pub fn retain_unknown_records(&mut self, stream: &str, records: &[UnknownRecord]) {
109 self.retained_records
110 .extend(records.iter().map(|record| RetainedSourceRecord {
111 id: record.id.to_string(),
112 stream: stream.into(),
113 offset: record.offset,
114 byte_len: record.byte_len,
115 sha256: record.sha256.clone(),
116 data: record.data.clone(),
117 }));
118 }
119
120 pub fn attach_native_unknown_records(
122 &mut self,
123 ir: &mut CadIr,
124 format: &str,
125 records: &[UnknownRecord],
126 ) -> Result<(), NativeConvertError> {
127 self.retained_records.extend(records.iter().map(|record| {
128 let stream = self
129 .annotations
130 .provenance
131 .get(&record.id.0)
132 .and_then(|provenance| self.annotations.streams.get(provenance.stream as usize))
133 .cloned()
134 .unwrap_or_else(|| "source".into());
135 RetainedSourceRecord {
136 id: record.id.to_string(),
137 stream,
138 offset: record.offset,
139 byte_len: record.byte_len,
140 sha256: record.sha256.clone(),
141 data: record.data.clone(),
142 }
143 }));
144 let product_records = records
145 .iter()
146 .map(crate::NativeUnknownRecord::from)
147 .collect::<Vec<_>>();
148 ir.set_native_unknowns(format, &product_records)
149 }
150
151 pub fn native_unknown_records(
153 &self,
154 ir: &CadIr,
155 format: &str,
156 ) -> Result<Vec<UnknownRecord>, NativeConvertError> {
157 ir.native_unknowns(format)?
158 .into_iter()
159 .map(|reference| {
160 let retained = self.retained_record(&reference.id.0).ok_or_else(|| {
161 NativeConvertError::MissingRetainedSourceRecord(reference.id.0.clone())
162 })?;
163 Ok(UnknownRecord {
164 id: reference.id,
165 offset: retained.offset,
166 byte_len: retained.byte_len,
167 sha256: retained.sha256.clone(),
168 data: retained.data.clone(),
169 links: reference.links,
170 })
171 })
172 .collect()
173 }
174
175 pub fn validate(&self) -> Result<(), FidelityError> {
177 if self.version != SOURCE_FIDELITY_VERSION {
178 return Err(FidelityError::Version {
179 found: self.version.clone(),
180 });
181 }
182 let mut ids = std::collections::BTreeSet::new();
183 for record in &self.retained_records {
184 if !ids.insert(&record.id) {
185 return Err(FidelityError::DuplicateRecord {
186 id: record.id.clone(),
187 });
188 }
189 if let Some(data) = &record.data {
190 let actual = data.len() as u64;
191 if actual != record.byte_len {
192 return Err(FidelityError::Length {
193 id: record.id.clone(),
194 declared: record.byte_len,
195 actual,
196 });
197 }
198 if crate::hash::sha256_hex(data) != record.sha256 {
199 return Err(FidelityError::Digest {
200 id: record.id.clone(),
201 });
202 }
203 }
204 }
205 Ok(())
206 }
207
208 pub fn to_canonical_json(&self) -> Result<String, serde_json::Error> {
210 let mut canonical = self.clone();
211 canonical.finalize();
212 serde_json::to_string(&canonical)
213 }
214
215 pub fn from_json(text: &str) -> Result<Self, SourceFidelityParseError> {
217 let sidecar: Self = serde_json::from_str(text).map_err(SourceFidelityParseError::Json)?;
218 sidecar
219 .validate()
220 .map_err(SourceFidelityParseError::Fidelity)?;
221 Ok(sidecar)
222 }
223}
224
225#[derive(Debug, thiserror::Error)]
227pub enum SourceFidelityParseError {
228 #[error("invalid source-fidelity JSON: {0}")]
230 Json(serde_json::Error),
231 #[error(transparent)]
233 Fidelity(FidelityError),
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 fn record(id: &str, data: &[u8]) -> RetainedSourceRecord {
241 RetainedSourceRecord {
242 id: id.into(),
243 stream: "source".into(),
244 offset: 0,
245 byte_len: data.len() as u64,
246 sha256: crate::hash::sha256_hex(data),
247 data: Some(data.to_vec()),
248 }
249 }
250
251 #[test]
252 fn canonical_json_orders_retained_records() {
253 let sidecar = SourceFidelity {
254 retained_records: vec![record("b", &[2]), record("a", &[1])],
255 ..SourceFidelity::default()
256 };
257 let json = sidecar.to_canonical_json().expect("serialize sidecar");
258 let parsed = SourceFidelity::from_json(&json).expect("parse sidecar");
259 assert_eq!(parsed.retained_records[0].id, "a");
260 }
261
262 #[test]
263 fn validation_rejects_false_payload_metadata() {
264 let mut sidecar = SourceFidelity::default();
265 sidecar.retained_records.push(record("a", &[1, 2]));
266 sidecar.retained_records[0].sha256 = crate::hash::sha256_hex(&[2, 1]);
267 assert!(matches!(
268 sidecar.validate(),
269 Err(FidelityError::Digest { .. })
270 ));
271 }
272}