ic_build/cargo/release_metadata/
mod.rs1use super::lock_projection::{
8 LockProjectionError, WorkspaceVersionProjection, verify_workspace_release_projection,
9};
10use std::{collections::HashSet, fmt, ops::Range};
11use toml_edit::{Document, Item, Value};
12
13#[derive(Clone, Copy, Debug)]
15pub struct DependencyVersionProjection<'a> {
16 pub alias: &'a str,
18 pub previous: &'a str,
20 pub next: &'a str,
22}
23
24#[derive(Debug)]
26pub enum ReleaseMetadataError {
27 InvalidSelection(&'static str),
29 Parse(toml_edit::TomlError),
31 Projection(LockProjectionError),
33}
34impl fmt::Display for ReleaseMetadataError {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 match self {
37 Self::InvalidSelection(reason) => f.write_str(reason),
38 Self::Parse(error) => error.fmt(f),
39 Self::Projection(error) => error.fmt(f),
40 }
41 }
42}
43impl std::error::Error for ReleaseMetadataError {
44 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
45 match self {
46 Self::InvalidSelection(_) => None,
47 Self::Parse(error) => Some(error),
48 Self::Projection(error) => Some(error),
49 }
50 }
51}
52
53const fn refuse(reason: &'static str) -> ReleaseMetadataError {
54 ReleaseMetadataError::InvalidSelection(reason)
55}
56fn stable(version: &str) -> bool {
57 let mut count = 0;
58 version.split('.').all(|part| {
59 count += 1;
60 !part.is_empty()
61 && part.bytes().all(|byte| byte.is_ascii_digit())
62 && (part == "0" || !part.starts_with('0'))
63 }) && count == 3
64}
65fn edit(
66 value: &Value,
67 previous: &str,
68 next: &str,
69) -> Result<(Range<usize>, String), ReleaseMetadataError> {
70 if value.as_str() != Some(previous) {
71 return Err(refuse("selected version differs from the admitted input"));
72 }
73 let span = value
74 .span()
75 .ok_or_else(|| refuse("selected literal has no source span"))?;
76 Ok((span, Value::from(next).to_string()))
77}
78fn apply(
79 input: &str,
80 mut edits: Vec<(Range<usize>, String)>,
81) -> Result<String, ReleaseMetadataError> {
82 edits.sort_by_key(|edit| std::cmp::Reverse(edit.0.start));
83 let mut output = input.to_owned();
84 let mut boundary = input.len();
85 for (span, replacement) in edits {
86 if span.end > boundary || input.get(span.clone()).is_none() {
87 return Err(refuse("overlapping or invalid selected literal spans"));
88 }
89 boundary = span.start;
90 output.replace_range(span, &replacement);
91 }
92 Document::parse(output.as_str()).map_err(ReleaseMetadataError::Parse)?;
93 Ok(output)
94}
95
96pub fn render_workspace_manifest(
106 input: &str,
107 previous: &str,
108 next: &str,
109 dependencies: &[DependencyVersionProjection<'_>],
110) -> Result<String, ReleaseMetadataError> {
111 if !stable(previous) || !stable(next) {
112 return Err(refuse("expected canonical stable workspace versions"));
113 }
114 let document = Document::parse(input).map_err(ReleaseMetadataError::Parse)?;
115 let workspace = document
116 .as_item()
117 .get("workspace")
118 .ok_or_else(|| refuse("missing workspace"))?;
119 let version = workspace
120 .get("package")
121 .and_then(|item| item.get("version"))
122 .and_then(Item::as_value)
123 .ok_or_else(|| refuse("missing workspace.package.version"))?;
124 let mut edits = vec![edit(version, previous, next)?];
125 let mut seen = HashSet::new();
126 for dependency in dependencies {
127 if dependency.alias.is_empty()
128 || !seen.insert(dependency.alias)
129 || dependency.previous.is_empty()
130 || dependency.next.is_empty()
131 {
132 return Err(refuse("invalid or duplicate dependency selection"));
133 }
134 let version = workspace
135 .get("dependencies")
136 .and_then(|item| item.get(dependency.alias))
137 .and_then(|item| item.get("version"))
138 .and_then(Item::as_value)
139 .ok_or_else(|| refuse("missing selected dependency version"))?;
140 edits.push(edit(version, dependency.previous, dependency.next)?);
141 }
142 apply(input, edits)
143}
144
145pub fn render_workspace_lock(
155 input: &str,
156 projections: &[WorkspaceVersionProjection<'_>],
157) -> Result<String, ReleaseMetadataError> {
158 if projections.is_empty()
159 || projections
160 .iter()
161 .any(|item| !stable(item.previous) || !stable(item.next) || item.previous == item.next)
162 {
163 return Err(refuse("expected distinct canonical stable lock versions"));
164 }
165 match verify_workspace_release_projection(input, input, projections) {
168 Err(LockProjectionError::ChangedInputs) => {}
169 Err(error) => return Err(ReleaseMetadataError::Projection(error)),
170 Ok(()) => return Err(refuse("lock selection changes no versions")),
171 }
172 let document = Document::parse(input).map_err(ReleaseMetadataError::Parse)?;
173 if !matches!(
174 document.as_item().get("version").and_then(Item::as_integer),
175 Some(3 | 4)
176 ) {
177 return Err(refuse("expected Cargo lock format 3 or 4"));
178 }
179 let packages = document
180 .as_item()
181 .get("package")
182 .and_then(Item::as_array_of_tables)
183 .ok_or_else(|| refuse("missing lock packages"))?;
184 let mut edits = Vec::new();
185 for package in packages {
186 if package.get("source").is_none()
187 && let Some(projection) = projections
188 .iter()
189 .find(|item| package.get("name").and_then(Item::as_str) == Some(item.package))
190 {
191 let value = package
192 .get("version")
193 .and_then(Item::as_value)
194 .ok_or_else(|| refuse("missing local package version"))?;
195 edits.push(edit(value, projection.previous, projection.next)?);
196 }
197 if let Some(dependencies) = package.get("dependencies").and_then(Item::as_array) {
198 for dependency in dependencies {
199 for projection in projections {
200 let previous = format!("{} {}", projection.package, projection.previous);
201 if dependency.as_str() == Some(previous.as_str()) {
202 edits.push(edit(
203 dependency,
204 &previous,
205 &format!("{} {}", projection.package, projection.next),
206 )?);
207 }
208 }
209 }
210 }
211 }
212 let output = apply(input, edits)?;
213 verify_workspace_release_projection(input, &output, projections)
214 .map_err(ReleaseMetadataError::Projection)?;
215 Ok(output)
216}
217
218#[cfg(test)]
219mod tests;