Skip to main content

powerio_core/
records.rs

1use std::collections::BTreeMap;
2use std::fmt;
3
4use serde::Deserialize;
5use serde_json::Value;
6
7use crate::validation::{valid_nonempty_text, valid_rfc6901_pointer};
8use crate::{Error, FormatId};
9
10macro_rules! record_id {
11    ($name:ident, $label:literal) => {
12        #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
13        pub struct $name(Box<str>);
14
15        impl $name {
16            pub fn new(value: impl Into<String>) -> Result<Self, Error> {
17                let value = value.into();
18                if !valid_nonempty_text(&value) {
19                    return Err(Error::new(
20                        &crate::codes::REQUEST_RECORD_INVALID_IDENTIFIER,
21                        concat!($label, " must be nonempty and bounded"),
22                    ));
23                }
24                Ok(Self(value.into_boxed_str()))
25            }
26
27            #[must_use]
28            pub fn as_str(&self) -> &str {
29                &self.0
30            }
31        }
32
33        impl fmt::Display for $name {
34            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
35                formatter.write_str(&self.0)
36            }
37        }
38
39        impl serde::Serialize for $name {
40            fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
41                serializer.serialize_str(&self.0)
42            }
43        }
44
45        // A stored identifier is validated on the way in, with the byte bound
46        // applied before the text is retained, so a malformed document fails
47        // at the field rather than reaching a record.
48        impl<'de> serde::Deserialize<'de> for $name {
49            fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
50                use serde::de::DeserializeSeed;
51                let value = crate::bounded::BoundedStr {
52                    what: $label,
53                    max_bytes: crate::validation::MAX_IDENTIFIER_BYTES,
54                }
55                .deserialize(deserializer)?;
56                Self::new(value).map_err(serde::de::Error::custom)
57            }
58        }
59    };
60}
61
62record_id!(SourceId, "source ID");
63record_id!(DiagnosticId, "diagnostic ID");
64record_id!(HistoryId, "history ID");
65
66/// Program identity recorded with a module.
67#[derive(Clone, Debug, PartialEq, Eq)]
68pub struct Producer {
69    name: Box<str>,
70    version: Box<str>,
71}
72
73impl Producer {
74    pub fn new(name: impl Into<String>, version: impl Into<String>) -> Result<Self, Error> {
75        let name = name.into();
76        let version = version.into();
77        if !valid_nonempty_text(&name) || !valid_nonempty_text(&version) {
78            return Err(Error::new(
79                &crate::codes::REQUEST_RECORD_INVALID_IDENTIFIER,
80                "producer name and version must be nonempty and bounded",
81            ));
82        }
83        Ok(Self {
84            name: name.into_boxed_str(),
85            version: version.into_boxed_str(),
86        })
87    }
88
89    pub(crate) fn powerio() -> Self {
90        Self {
91            name: "powerio".into(),
92            version: env!("CARGO_PKG_VERSION").into(),
93        }
94    }
95
96    #[must_use]
97    pub fn name(&self) -> &str {
98        &self.name
99    }
100
101    #[must_use]
102    pub fn version(&self) -> &str {
103        &self.version
104    }
105}
106
107#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
108#[non_exhaustive]
109pub enum DigestAlgorithm {
110    Sha256,
111}
112
113impl DigestAlgorithm {
114    #[must_use]
115    pub const fn as_str(self) -> &'static str {
116        match self {
117            Self::Sha256 => "sha256",
118        }
119    }
120}
121
122/// Validated digest attached to a stored source descriptor.
123#[derive(Clone, Debug, PartialEq, Eq)]
124pub struct Digest {
125    algorithm: DigestAlgorithm,
126    value: Box<str>,
127}
128
129impl Digest {
130    pub fn sha256(value: impl Into<String>) -> Result<Self, Error> {
131        let value = value.into();
132        if value.len() != 64
133            || !value
134                .bytes()
135                .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
136        {
137            return Err(Error::new(
138                &crate::codes::REQUEST_RECORD_INVALID_DIGEST,
139                "a SHA-256 digest must contain 64 lowercase hexadecimal characters",
140            ));
141        }
142        Ok(Self {
143            algorithm: DigestAlgorithm::Sha256,
144            value: value.into_boxed_str(),
145        })
146    }
147
148    #[must_use]
149    pub const fn algorithm(&self) -> DigestAlgorithm {
150        self.algorithm
151    }
152
153    #[must_use]
154    pub fn value(&self) -> &str {
155        &self.value
156    }
157}
158
159/// Durable description of one source buffer.
160#[derive(Clone, Debug, PartialEq, Eq)]
161pub struct SourceDescriptor {
162    id: SourceId,
163    name: Box<str>,
164    byte_length: u64,
165    format: Option<FormatId>,
166    digest: Option<Digest>,
167}
168
169impl SourceDescriptor {
170    pub fn new(id: SourceId, name: impl Into<String>, byte_length: u64) -> Result<Self, Error> {
171        let name = name.into();
172        if !valid_nonempty_text(&name) {
173            return Err(Error::new(
174                &crate::codes::REQUEST_RECORD_INVALID_IDENTIFIER,
175                "source name must be nonempty and bounded",
176            ));
177        }
178        Ok(Self {
179            id,
180            name: name.into_boxed_str(),
181            byte_length,
182            format: None,
183            digest: None,
184        })
185    }
186
187    #[must_use]
188    pub fn id(&self) -> &SourceId {
189        &self.id
190    }
191
192    #[must_use]
193    pub fn name(&self) -> &str {
194        &self.name
195    }
196
197    #[must_use]
198    pub const fn byte_length(&self) -> u64 {
199        self.byte_length
200    }
201
202    #[must_use]
203    pub const fn format(&self) -> Option<&FormatId> {
204        self.format.as_ref()
205    }
206
207    #[must_use]
208    pub const fn digest(&self) -> Option<&Digest> {
209        self.digest.as_ref()
210    }
211
212    #[must_use]
213    pub fn with_format(mut self, format: FormatId) -> Self {
214        self.format = Some(format);
215        self
216    }
217
218    #[must_use]
219    pub fn with_digest(mut self, digest: Digest) -> Self {
220        self.digest = Some(digest);
221        self
222    }
223}
224
225/// Half open byte range in one module source.
226#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
227pub struct SourceSpan {
228    source: SourceId,
229    byte_start: u64,
230    byte_end: u64,
231}
232
233impl SourceSpan {
234    pub fn new(source: SourceId, byte_start: u64, byte_end: u64) -> Result<Self, Error> {
235        if byte_start > byte_end {
236            return Err(Error::new(
237                &crate::codes::REQUEST_RECORD_INVALID_SPAN,
238                format!("source span {byte_start}..{byte_end} is reversed"),
239            ));
240        }
241        Ok(Self {
242            source,
243            byte_start,
244            byte_end,
245        })
246    }
247
248    #[must_use]
249    pub fn source(&self) -> &SourceId {
250        &self.source
251    }
252
253    #[must_use]
254    pub const fn byte_start(&self) -> u64 {
255        self.byte_start
256    }
257
258    #[must_use]
259    pub const fn byte_end(&self) -> u64 {
260        self.byte_end
261    }
262}
263
264#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
265#[non_exhaustive]
266pub enum SourceRelation {
267    Exact,
268    Defaulted,
269    Inferred,
270    ConvertedUnits,
271    Aggregated,
272    Split,
273    Synthetic,
274    Transformed,
275    RetainedExtra,
276}
277
278impl SourceRelation {
279    #[must_use]
280    pub const fn allows_empty_spans(self) -> bool {
281        matches!(self, Self::Defaulted | Self::Synthetic | Self::Transformed)
282    }
283}
284
285/// Relation between one typed value target and its source bytes.
286#[derive(Clone, Debug, PartialEq, Eq)]
287pub struct SourceMapEntry {
288    target: Box<str>,
289    relation: SourceRelation,
290    spans: Vec<SourceSpan>,
291}
292
293impl SourceMapEntry {
294    pub fn new(
295        target: impl Into<String>,
296        relation: SourceRelation,
297        spans: Vec<SourceSpan>,
298    ) -> Result<Self, Error> {
299        let target = target.into();
300        if !valid_rfc6901_pointer(&target) {
301            return Err(Error::new(
302                &crate::codes::REQUEST_RECORD_INVALID_POINTER,
303                "a source map target must be an RFC 6901 pointer",
304            ));
305        }
306        if spans.is_empty() && !relation.allows_empty_spans() {
307            return Err(Error::new(
308                &crate::codes::REQUEST_RECORD_INVALID_SPAN,
309                "this source relation requires at least one byte span",
310            ));
311        }
312        if spans.len() > crate::validation::MAX_SOURCE_MAP_SPANS {
313            return Err(Error::new(
314                &crate::codes::REQUEST_RECORD_TOO_LARGE,
315                format!(
316                    "a source map entry carries more than {} byte spans",
317                    crate::validation::MAX_SOURCE_MAP_SPANS
318                ),
319            ));
320        }
321        Ok(Self {
322            target: target.into_boxed_str(),
323            relation,
324            spans,
325        })
326    }
327
328    #[must_use]
329    pub fn target(&self) -> &str {
330        &self.target
331    }
332
333    #[must_use]
334    pub const fn relation(&self) -> SourceRelation {
335        self.relation
336    }
337
338    #[must_use]
339    pub fn spans(&self) -> &[SourceSpan] {
340        &self.spans
341    }
342}
343
344#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
345#[non_exhaustive]
346pub enum HistoryKind {
347    Parse,
348    Upgrade,
349    Transform,
350    Edit,
351    Repair,
352}
353
354/// Structured description of an operation that produced the current value.
355#[derive(Clone, Debug, PartialEq)]
356pub struct HistoryEntry {
357    id: HistoryId,
358    kind: HistoryKind,
359    name: Box<str>,
360    input_kind: Option<Box<str>>,
361    output_kind: Option<Box<str>>,
362    parameters: BTreeMap<String, Value>,
363    assumptions: Vec<String>,
364    losses: Vec<String>,
365}
366
367impl HistoryEntry {
368    pub fn new(id: HistoryId, kind: HistoryKind, name: impl Into<String>) -> Result<Self, Error> {
369        let name = name.into();
370        if !valid_nonempty_text(&name) {
371            return Err(Error::new(
372                &crate::codes::REQUEST_RECORD_INVALID_IDENTIFIER,
373                "history operation name must be nonempty and bounded",
374            ));
375        }
376        Ok(Self {
377            id,
378            kind,
379            name: name.into_boxed_str(),
380            input_kind: None,
381            output_kind: None,
382            parameters: BTreeMap::new(),
383            assumptions: Vec::new(),
384            losses: Vec::new(),
385        })
386    }
387
388    #[must_use]
389    pub fn id(&self) -> &HistoryId {
390        &self.id
391    }
392
393    #[must_use]
394    pub const fn kind(&self) -> HistoryKind {
395        self.kind
396    }
397
398    #[must_use]
399    pub fn name(&self) -> &str {
400        &self.name
401    }
402
403    #[must_use]
404    pub fn input_kind(&self) -> Option<&str> {
405        self.input_kind.as_deref()
406    }
407
408    #[must_use]
409    pub fn output_kind(&self) -> Option<&str> {
410        self.output_kind.as_deref()
411    }
412
413    #[must_use]
414    pub const fn parameters(&self) -> &BTreeMap<String, Value> {
415        &self.parameters
416    }
417
418    #[must_use]
419    pub fn assumptions(&self) -> &[String] {
420        &self.assumptions
421    }
422
423    #[must_use]
424    pub fn losses(&self) -> &[String] {
425        &self.losses
426    }
427
428    pub fn with_input_kind(mut self, kind: impl Into<String>) -> Result<Self, Error> {
429        self.input_kind = Some(validated_kind(kind.into())?);
430        Ok(self)
431    }
432
433    pub fn with_output_kind(mut self, kind: impl Into<String>) -> Result<Self, Error> {
434        self.output_kind = Some(validated_kind(kind.into())?);
435        Ok(self)
436    }
437
438    pub fn with_parameters(mut self, parameters: BTreeMap<String, Value>) -> Result<Self, Error> {
439        if parameters.len() > crate::validation::MAX_HISTORY_PARAMETERS {
440            return Err(history_too_large(
441                "parameters",
442                crate::validation::MAX_HISTORY_PARAMETERS,
443            ));
444        }
445        if parameters.keys().any(|key| !valid_nonempty_text(key)) {
446            return Err(Error::new(
447                &crate::codes::REQUEST_RECORD_INVALID_IDENTIFIER,
448                "a history parameter key must be nonempty and bounded",
449            ));
450        }
451        self.parameters = parameters;
452        Ok(self)
453    }
454
455    pub fn with_assumption(mut self, assumption: impl Into<String>) -> Result<Self, Error> {
456        self.assumptions = push_history_note(self.assumptions, assumption.into(), "assumptions")?;
457        Ok(self)
458    }
459
460    pub fn with_loss(mut self, loss: impl Into<String>) -> Result<Self, Error> {
461        self.losses = push_history_note(self.losses, loss.into(), "losses")?;
462        Ok(self)
463    }
464}
465
466fn history_too_large(what: &str, limit: usize) -> Error {
467    Error::new(
468        &crate::codes::REQUEST_RECORD_TOO_LARGE,
469        format!("a history entry carries more than {limit} {what}"),
470    )
471}
472
473fn push_history_note(
474    mut notes: Vec<String>,
475    note: String,
476    what: &'static str,
477) -> Result<Vec<String>, Error> {
478    if notes.len() >= crate::validation::MAX_HISTORY_NOTES {
479        return Err(history_too_large(
480            what,
481            crate::validation::MAX_HISTORY_NOTES,
482        ));
483    }
484    if !valid_nonempty_text(&note) {
485        return Err(Error::new(
486            &crate::codes::REQUEST_RECORD_INVALID_IDENTIFIER,
487            format!("a history {what} note must be nonempty and bounded"),
488        ));
489    }
490    notes.push(note);
491    Ok(notes)
492}
493
494fn validated_kind(kind: String) -> Result<Box<str>, Error> {
495    if !valid_nonempty_text(&kind) {
496        return Err(Error::new(
497            &crate::codes::REQUEST_RECORD_INVALID_IDENTIFIER,
498            "a history value kind must be nonempty and bounded",
499        ));
500    }
501    Ok(kind.into_boxed_str())
502}
503
504impl<'de> serde::Deserialize<'de> for SourceSpan {
505    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
506        #[derive(Deserialize)]
507        struct Wire {
508            source: SourceId,
509            byte_start: u64,
510            byte_end: u64,
511        }
512        let wire = Wire::deserialize(deserializer)?;
513        Self::new(wire.source, wire.byte_start, wire.byte_end).map_err(serde::de::Error::custom)
514    }
515}
516
517#[cfg(test)]
518mod tests {
519    use super::*;
520
521    #[test]
522    fn identifiers_and_digests_are_strict() {
523        assert!(SourceId::new("").is_err());
524        assert!(SourceId::new("x\0y").is_err());
525        assert!(SourceId::new("x".repeat(65_537)).is_err());
526        assert!(SourceId::new("Case A").is_ok());
527        assert!(Digest::sha256("a".repeat(64)).is_ok());
528        assert!(Digest::sha256("A".repeat(64)).is_err());
529        assert!(Digest::sha256("a".repeat(63)).is_err());
530    }
531
532    #[test]
533    fn source_map_spans_obey_relation_rules() {
534        let id = SourceId::new("input").unwrap();
535        assert!(SourceSpan::new(id.clone(), 2, 1).is_err());
536        assert!(SourceMapEntry::new("/bus/0", SourceRelation::Exact, Vec::new()).is_err());
537        assert!(SourceMapEntry::new("/bus/0", SourceRelation::Defaulted, Vec::new()).is_ok());
538        assert!(SourceMapEntry::new("bad", SourceRelation::Synthetic, Vec::new()).is_err());
539    }
540}