mos_eval/
semantic_hash.rs1use std::collections::BTreeMap;
4use std::path::PathBuf;
5
6use mos_core::{AttrValue, ContentHasher, Document, NodeKind};
7
8use crate::image_lower::{
9 BITS_PER_COMPONENT_ATTR, COLOR_SPACE_ATTR, PIXEL_HEIGHT_ATTR, PIXEL_WIDTH_ATTR, PIXELS_ATTR,
10};
11use crate::{ExternalDependency, LABEL_SPAN_END_ATTR, LABEL_SPAN_START_ATTR};
12
13pub(crate) fn stamp(
16 document: &mut Document,
17 dependencies: &[ExternalDependency],
18 bibliography_loaded: bool,
19) {
20 let files: BTreeMap<_, _> = dependencies
21 .iter()
22 .map(|dependency| {
23 (
24 dependency.path.as_path(),
25 dependency
26 .fingerprint
27 .map(crate::ResourceFingerprint::content),
28 )
29 })
30 .collect();
31 document.update_content_hashes(|node| {
32 let mut hasher = ContentHasher::new();
33 hasher
34 .field(b"mos-eval/authored-node/v1")
35 .field(kind_tag(node.kind));
36 for (key, value) in &node.attributes {
40 if matches!(
41 key.as_str(),
42 LABEL_SPAN_START_ATTR | LABEL_SPAN_END_ATTR | "resolved_path"
43 ) || (node.kind == NodeKind::Image
44 && matches!(
45 key.as_str(),
46 PIXELS_ATTR
47 | PIXEL_WIDTH_ATTR
48 | PIXEL_HEIGHT_ATTR
49 | COLOR_SPACE_ATTR
50 | BITS_PER_COMPONENT_ATTR
51 ))
52 || (matches!(
53 node.kind,
54 NodeKind::Reference | NodeKind::PageReference | NodeKind::Citation
55 ) && key == "text")
56 {
57 continue;
58 }
59 hasher.field(b"attr").field(key.as_bytes());
60 hash_value(&mut hasher, value);
61 }
62 if matches!(node.kind, NodeKind::Image | NodeKind::Bibliography) {
63 let path = if node.kind == NodeKind::Bibliography {
64 match node
67 .attributes
68 .get("resolved_path")
69 .filter(|_| bibliography_loaded)
70 {
71 Some(AttrValue::Str(path)) => Some(PathBuf::from(path)),
72 _ => None,
73 }
74 } else {
75 match node.attributes.get("src") {
78 Some(AttrValue::Str(src)) => {
79 mos_core::resolve_source_path(src, &node.span.file).ok()
80 }
81 _ => None,
82 }
83 };
84 hasher.field(b"external-input");
85 match path.as_deref().and_then(|path| files.get(path)) {
86 Some(Some(content)) => {
87 hasher.field(b"read").field(&content.0.to_le_bytes());
88 }
89 Some(None) => {
90 hasher.field(b"unreadable");
91 }
92 None => {
93 hasher.field(b"unloaded");
94 }
95 }
96 }
97 hasher.finish()
98 });
99}
100
101fn kind_tag(kind: NodeKind) -> &'static [u8] {
103 match kind {
104 NodeKind::Document => b"document",
105 NodeKind::Section => b"section",
106 NodeKind::Paragraph => b"paragraph",
107 NodeKind::Text => b"text",
108 NodeKind::Emphasis => b"emphasis",
109 NodeKind::Strong => b"strong",
110 NodeKind::BoldItalic => b"bold-italic",
111 NodeKind::Math => b"math",
112 NodeKind::Equation => b"equation",
113 NodeKind::Figure => b"figure",
114 NodeKind::Image => b"image",
115 NodeKind::Table => b"table",
116 NodeKind::Citation => b"citation",
117 NodeKind::Reference => b"reference",
118 NodeKind::PageReference => b"page-reference",
119 NodeKind::Theorem => b"theorem",
120 NodeKind::Footnote => b"footnote",
121 NodeKind::Bibliography => b"bibliography",
122 NodeKind::Raw => b"raw",
123 NodeKind::List => b"list",
124 NodeKind::ListItem => b"list-item",
125 NodeKind::HardBreak => b"hard-break",
126 }
127}
128
129fn hash_value(hasher: &mut ContentHasher, value: &AttrValue) {
130 match value {
131 AttrValue::Bool(value) => {
132 hasher.field(b"bool").u32(u32::from(*value));
133 }
134 AttrValue::Int(value) => {
135 hasher.field(b"int").field(&value.to_le_bytes());
136 }
137 AttrValue::Float(value) => {
138 hasher
139 .field(b"float")
140 .field(&float_bits(*value).to_le_bytes());
141 }
142 AttrValue::Str(value) => {
143 hasher.field(b"str").field(value.as_bytes());
144 }
145 AttrValue::List(values) => {
146 hasher.field(b"list");
147 for value in values {
148 hash_value(hasher, value);
149 }
150 hasher.field(b"end-list");
151 }
152 AttrValue::Length(value) => {
153 hasher
156 .field(b"length")
157 .field(&float_bits((value * 64.0).round()).to_le_bytes());
158 }
159 AttrValue::Bytes(value) => {
160 hasher.field(b"bytes").field(value);
161 }
162 }
163}
164
165fn float_bits(value: f64) -> u64 {
166 if value.is_nan() {
167 0x7ff8_0000_0000_0000
169 } else if value == 0.0 {
170 0
171 } else {
172 value.to_bits()
173 }
174}
175
176#[cfg(test)]
177mod tests;