ic_build/cargo/packages/external_lock/
mod.rs1use crate::cargo::lockfile::{external_package, optional_string, required_string};
4use ic_host_artifacts::artifact::Sha256Digest;
5use std::{collections::BTreeMap, error::Error, fmt};
6
7type ExternalRows = BTreeMap<(String, String, String), Option<String>>;
8
9#[derive(Debug)]
11#[non_exhaustive]
12pub enum ExternalLockError {
13 LimitExceeded {
15 input: &'static str,
17 limit: usize,
19 },
20 Utf8 {
22 input: &'static str,
24 source: std::str::Utf8Error,
26 },
27 Parse {
29 input: &'static str,
31 source: toml::de::Error,
33 },
34 MissingPackages {
36 input: &'static str,
38 },
39 InvalidPackage {
41 input: &'static str,
43 index: usize,
45 reason: &'static str,
47 },
48 DuplicateSelection {
50 input: &'static str,
52 package: String,
54 version: String,
56 },
57 SelectionChanged {
59 package: String,
61 version: String,
63 },
64}
65
66impl fmt::Display for ExternalLockError {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 match self {
69 Self::LimitExceeded { input, limit } => {
70 write!(f, "{input} lockfile exceeds {limit} bytes")
71 }
72 Self::Utf8 { input, source } => write!(f, "invalid {input} lockfile UTF-8: {source}"),
73 Self::Parse { input, source } => write!(f, "invalid {input} lockfile: {source}"),
74 Self::MissingPackages { input } => write!(f, "missing {input} lock packages"),
75 Self::InvalidPackage {
76 input,
77 index,
78 reason,
79 } => {
80 write!(f, "{input} lock package {index}: {reason}")
81 }
82 Self::DuplicateSelection {
83 input,
84 package,
85 version,
86 } => {
87 write!(
88 f,
89 "duplicate {input} package selection: {package}@{version}"
90 )
91 }
92 Self::SelectionChanged { package, version } => {
93 write!(
94 f,
95 "packaging changed external selection: {package}@{version}"
96 )
97 }
98 }
99 }
100}
101
102impl Error for ExternalLockError {
103 fn source(&self) -> Option<&(dyn Error + 'static)> {
104 match self {
105 Self::Utf8 { source, .. } => Some(source),
106 Self::Parse { source, .. } => Some(source),
107 _ => None,
108 }
109 }
110}
111
112pub fn verify_external_lock_subset(
128 original: &[u8],
129 candidate: &[u8],
130 max_bytes: usize,
131) -> Result<(), ExternalLockError> {
132 for (input, bytes) in [("original", original), ("candidate", candidate)] {
133 if bytes.len() > max_bytes {
134 return Err(ExternalLockError::LimitExceeded {
135 input,
136 limit: max_bytes,
137 });
138 }
139 }
140 let original = external_rows(original, "original")?;
141 let candidate = external_rows(candidate, "candidate")?;
142 verify_rows(&original, &candidate)
143}
144
145fn external_rows(bytes: &[u8], input: &'static str) -> Result<ExternalRows, ExternalLockError> {
146 let text =
147 std::str::from_utf8(bytes).map_err(|source| ExternalLockError::Utf8 { input, source })?;
148 let lock: toml::Value =
149 toml::from_str(text).map_err(|source| ExternalLockError::Parse { input, source })?;
150 let mut rows = BTreeMap::new();
151 for (index, package) in lock
152 .get("package")
153 .and_then(toml::Value::as_array)
154 .ok_or(ExternalLockError::MissingPackages { input })?
155 .iter()
156 .enumerate()
157 {
158 let invalid = |reason| ExternalLockError::InvalidPackage {
159 input,
160 index,
161 reason,
162 };
163 let external = external_package(package).map_err(invalid)?;
166 let Some(table) = external else { continue };
167 let text = |key| required_string(table, key).map_err(invalid);
168 let source = text("source")?;
169 let checksum = optional_string(table, "checksum").map_err(invalid)?;
170 if source.starts_with("registry+") {
171 checksum
172 .ok_or_else(|| invalid("missing lock checksum"))?
173 .parse::<Sha256Digest>()
174 .map_err(|_| invalid("invalid package checksum"))?;
175 }
176 let name = text("name")?;
177 let version = text("version")?;
178 if rows
179 .insert(
180 (name.into(), version.into(), source.into()),
181 checksum.map(str::to_owned),
182 )
183 .is_some()
184 {
185 return Err(ExternalLockError::DuplicateSelection {
186 input,
187 package: name.into(),
188 version: version.into(),
189 });
190 }
191 }
192 Ok(rows)
193}
194
195fn verify_rows(original: &ExternalRows, candidate: &ExternalRows) -> Result<(), ExternalLockError> {
196 for (key @ (package, version, _), checksum) in candidate {
197 if original.get(key) != Some(checksum) {
198 return Err(ExternalLockError::SelectionChanged {
199 package: package.clone(),
200 version: version.clone(),
201 });
202 }
203 }
204 Ok(())
205}
206
207pub(super) fn admit_lock(entry: &[u8], payload: &[u8]) -> Result<(), super::Cause> {
208 let entry = external_rows(entry, "original")?;
209 let payload = external_rows(payload, "candidate")?;
210 if payload.is_empty() {
211 return Err("empty package external graph".into());
212 }
213 verify_rows(&entry, &payload)?;
214 Ok(())
215}
216
217#[cfg(test)]
218mod tests;