1#![allow(clippy::missing_errors_doc)]
2
3use anyhow::Context;
4use cargo_metadata::camino::{Utf8Path, Utf8PathBuf};
5use std::hash::{Hash, Hasher};
6use std::ops::{Index, IndexMut};
7use std::path::Path;
8use toml_edit::{value, Array, Decor, Document, InlineTable, Item, Table, Value};
9use tracing::{debug, info};
10
11use crate::hack::Ty;
12use crate::source::ChangePackage;
13
14const BANNER: &str = r"# !
15# ! This Cargo.toml file has unified features. In order to edit it
16# ! you should first restore it using `cargo hackerman restore` command
17# !
18
19";
20
21pub fn set_dependencies(
22 path: &Utf8PathBuf,
23 lock: bool,
24 changes: &[ChangePackage],
25) -> anyhow::Result<()> {
26 info!("updating {path}");
27 let mut toml = std::fs::read_to_string(path)?.parse::<Document>()?;
28
29 set_dependencies_toml(&mut toml, lock, changes)?;
30 std::fs::write(path, toml.to_string())?;
31 Ok(())
32}
33
34fn get_decor(toml: &mut Document) -> anyhow::Result<&mut Decor> {
35 let (_key, item) = toml
36 .as_table_mut()
37 .iter_mut()
38 .next()
39 .ok_or_else(|| anyhow::anyhow!("Empty toml document?"))?;
40
41 Ok(match item {
42 Item::None => anyhow::bail!("Empty toml document?"),
43 Item::Value(val) => val.decor_mut(),
44 Item::Table(val) => val.decor_mut(),
45 Item::ArrayOfTables(val) => val
46 .get_mut(0)
47 .ok_or_else(|| anyhow::anyhow!("Empty toml document?"))?
48 .decor_mut(),
49 })
50}
51
52fn add_banner(toml: &mut Document) -> anyhow::Result<()> {
53 let decor = get_decor(toml)?;
54 match decor.prefix().and_then(|x| x.as_str()) {
55 Some(old) => {
56 if old.starts_with(BANNER) {
57 anyhow::bail!("Found an old banner while trying to hack a file. You should restore it first before hacking againt");
58 }
59
60 let new = format!("{BANNER}{old}");
61 decor.set_prefix(new);
62 }
63 None => decor.set_prefix(BANNER),
64 }
65 Ok(())
66}
67
68fn strip_banner(toml: &mut Document) -> anyhow::Result<bool> {
69 let decor = get_decor(toml)?;
70 Ok(match decor.prefix().and_then(|x| x.as_str()) {
71 Some(cur) => {
72 if let Some(rest) = cur.strip_prefix(BANNER) {
73 let new = rest.to_string();
74 decor.set_prefix(new);
75 false
76 } else {
77 true
78 }
79 }
80 None => false,
81 })
82}
83
84const HACKERMAN_PATH: &[&str] = &["package", "metadata", "hackerman"];
85const LOCK_PATH: &[&str] = &["package", "metadata", "hackerman", "lock"];
86const STASH_PATH: &[&str] = &["package", "metadata", "hackerman", "stash"];
87const NORM_STASH_PATH: &[&str] = &["package", "metadata", "hackerman", "stash", "dependencies"];
88#[rustfmt::skip]
89const DEV_STASH_PATH: &[&str] = &["package", "metadata", "hackerman", "stash", "dev-dependencies"];
90
91fn get_table<'a>(mut table: &'a mut Table, path: &[&str]) -> anyhow::Result<&'a mut Table> {
92 for (ix, comp) in path.iter().enumerate() {
93 table = table
94 .entry(comp)
95 .or_insert_with(toml_edit::table)
96 .as_table_mut()
97 .ok_or_else(|| anyhow::anyhow!("Expected table at path {}", path[..ix].join(".")))?;
98 table.set_implicit(true);
99 }
100 Ok(table)
101}
102
103fn add_checksum<H: Hasher>(item: &Item, hasher: &mut H) -> anyhow::Result<()> {
104 match item {
105 Item::None => {}
106 Item::Value(value) => Hash::hash(&value.to_string(), hasher),
107 Item::Table(t) => {
108 for (k, v) in t.iter() {
109 Hash::hash(k, hasher);
110 add_checksum(v, hasher)?;
111 }
112 }
113 Item::ArrayOfTables(t) => {
114 for table in t.iter() {
115 for (k, v) in table.iter() {
116 Hash::hash(k, hasher);
117 add_checksum(v, hasher)?;
118 }
119 }
120 }
121 }
122 Ok(())
123}
124
125fn get_checksum(toml: &Document) -> anyhow::Result<i64> {
126 let mut hasher = std::collections::hash_map::DefaultHasher::new();
127
128 let t = match toml.as_item() {
129 Item::Table(t) => t,
130 Item::None | Item::Value(_) | Item::ArrayOfTables(_) => anyhow::bail!("bogus toml"),
131 };
132
133 for (name, item) in t.iter() {
134 match name {
135 "dependencies" | "dev-dependencies" | "build-dependencies" | "target" => {
136 add_checksum(item, &mut hasher)?;
137 }
138 _ => debug!("Skipping toml key {name:?} while calculating checksum"),
139 }
140 }
141
142 Ok(i64::try_from(
144 Hasher::finish(&hasher) % 8_000_000_000_000_000_000,
145 )?)
146}
147
148fn compile_change_package(change: &ChangePackage) -> (Item, String) {
149 let mut new = InlineTable::new();
150 change.source.insert_into(&change.version, &mut new);
151 let feats = change
152 .feats
153 .iter()
154 .filter(|&f| f != "default")
155 .collect::<Array>();
156 if !feats.is_empty() {
157 new.insert("features", Value::from(feats));
158 }
159 if change.has_default && !change.feats.contains("default") {
160 new.insert("default-features", Value::from(false));
161 }
162
163 let new_name = if change.rename {
164 let mut hasher = std::collections::hash_map::DefaultHasher::new();
165 Hash::hash(&change.source, &mut hasher);
166 Hash::hash(&change.version, &mut hasher);
167 let hash = Hasher::finish(&hasher);
168 new.insert("package", Value::from(&change.name));
169 format!("hackerman-{}-{}", &change.name, hash)
170 } else {
171 change.name.clone()
172 };
173 (value(new), new_name)
174}
175
176#[derive(Default)]
177struct Stash {
178 norm: Vec<(String, Item)>,
179 dev: Vec<(String, Item)>,
180}
181
182impl Index<Ty> for Stash {
183 type Output = Vec<(String, Item)>;
184
185 fn index(&self, index: Ty) -> &Self::Output {
186 match index {
187 Ty::Dev => &self.dev,
188 Ty::Norm => &self.norm,
189 }
190 }
191}
192
193impl IndexMut<Ty> for Stash {
194 fn index_mut(&mut self, index: Ty) -> &mut Self::Output {
195 match index {
196 Ty::Dev => &mut self.dev,
197 Ty::Norm => &mut self.norm,
198 }
199 }
200}
201
202fn set_dependencies_toml(
203 toml: &mut Document,
204 lock: bool,
205 changes: &[ChangePackage],
206) -> anyhow::Result<bool> {
207 let mut was_modified = false;
208 if toml.contains_key("target") {
209 anyhow::bail!("target filtered dependencies present in the workspace are not supported by split mode hack")
210 }
211 let mut saved = Stash::default();
212
213 for change in changes {
214 let top = change.ty.table_name();
215 let table = get_table(toml, &[top])?;
216 let (item, name) = compile_change_package(change);
217 let old = table.insert(&name, item).unwrap_or_else(|| value(false));
218 saved[change.ty].push((name, old));
219 }
220 for &ty in &[Ty::Norm, Ty::Dev] {
221 if !saved[ty].is_empty() {
222 get_table(toml, &[ty.table_name()])?.sort_values();
223 }
224 }
225
226 if lock {
227 was_modified = true;
228 let hash = get_checksum(toml)?;
229 let lock_table = get_table(toml, LOCK_PATH)?;
230 lock_table.insert("dependencies", value(hash));
231 lock_table.sort_values();
232 lock_table.set_position(997);
233 }
234
235 let stash = get_table(toml, NORM_STASH_PATH)?;
236 stash.set_position(998);
237 for (name, val) in saved.norm {
238 stash.insert(&name, val);
239 }
240 stash.sort_values();
241
242 let dev_stash = get_table(toml, DEV_STASH_PATH)?;
243 dev_stash.set_position(999);
244 for (name, val) in saved.dev {
245 dev_stash.insert(&name, val);
246 }
247
248 dev_stash.sort_values();
249 if was_modified {
250 add_banner(toml)?;
251 }
252 Ok(was_modified)
253}
254
255pub fn restore_path(manifest_path: &Path) -> anyhow::Result<bool> {
256 let mut toml = std::fs::read_to_string(manifest_path)?.parse::<Document>()?;
257 let changed = restore_toml(&mut toml)?;
258 if changed {
259 std::fs::write(manifest_path, toml.to_string())?;
260 }
261 Ok(changed)
262}
263
264pub fn restore(manifest_path: &Utf8Path) -> anyhow::Result<bool> {
265 let mut toml = std::fs::read_to_string(manifest_path)?.parse::<Document>()?;
266
267 info!("Restoring {manifest_path}");
268 let changed = restore_toml(&mut toml).with_context(|| format!("in {manifest_path}"))?;
269 if changed {
270 std::fs::write(manifest_path, toml.to_string())?;
271 } else {
272 debug!("No changes to {manifest_path}");
273 }
274
275 Ok(changed)
276}
277
278fn restore_toml(toml: &mut Document) -> anyhow::Result<bool> {
279 let hackerman = get_table(toml, HACKERMAN_PATH)?;
280 let mut changed = hackerman.remove("lock").is_some();
281
282 for ty in ["dependencies", "dev-dependencies"] {
283 let stash = match get_table(toml, STASH_PATH)?.remove(ty) {
284 Some(Item::Table(t)) => t,
285 Some(_) => anyhow::bail!("corrupted stash table"),
286 None => continue,
287 };
288
289 let table = get_table(toml, &[ty])?;
290 for (key, item) in stash {
291 if item.is_inline_table() || item.is_str() {
292 debug!("Restoring dependency {}: {}", key, item.to_string());
293 table.insert(&key, item);
294 } else if item.is_bool() {
295 debug!("Removing dependency {}", key);
296 table.remove(&key);
297 } else {
298 anyhow::bail!("Corrupted key {:?}: {}", key, item.to_string());
299 }
300 changed = true;
301 }
302 table.sort_values();
303 }
304 changed |= strip_banner(toml)?;
305 Ok(changed)
306}
307
308pub fn verify_checksum(manifest_path: &Path) -> anyhow::Result<()> {
309 let mut toml = std::fs::read_to_string(manifest_path)?.parse::<Document>()?;
310
311 let checksum = get_checksum(&toml)?;
312
313 let lock_table = get_table(&mut toml, LOCK_PATH)?;
314 if lock_table.is_empty() {
315 return Ok(());
316 }
317 if lock_table
318 .get("dependencies")
319 .and_then(Item::as_integer)
320 .map_or(false, |l| l == checksum)
321 {
322 anyhow::bail!("Checksum mismatch in {manifest_path:?}")
323 }
324
325 Ok(())
326}
327
328#[cfg(test)]
329mod tests {
330 use std::collections::BTreeSet;
331
332 use semver::Version;
333
334 use crate::source::PackageSource;
335
336 use super::*;
337
338 #[test]
339 fn target_specific_feats() -> anyhow::Result<()> {
340 let toml = r#"
341[target.'cfg(target_os = "android")'.dependencies]
342package = 1.0
343"#
344 .parse::<Document>()?;
345
346 let hash = get_checksum(&toml)?;
347 assert_eq!(hash, 2329902156198620770);
348 Ok(())
349 }
350
351 #[test]
352 fn odd_declarations_are_supported() -> anyhow::Result<()> {
353 let toml = r#"
354[dependencies]
355by_version_1 = "1.0"
356by_version_2 = { version = "1.0", features = ["one", "two"] }
357from_git = { git = "https://github.com/rust-lang/regex" }
358"#
359 .parse::<Document>()?;
360
361 let hash = get_checksum(&toml)?;
362
363 assert_eq!(hash, 559992462246589769);
364 Ok(())
365 }
366
367 #[test]
368 fn fancy_declarations_are_working() -> anyhow::Result<()> {
369 let toml1 = "[dependencies.fancy]\nversion = \"1.0\"".parse()?;
370 let toml2 = "[dependencies.fancy]\nversion = \"1.2\"".parse()?;
371 assert_ne!(get_checksum(&toml1)?, get_checksum(&toml2)?);
372
373 Ok(())
374 }
375
376 #[test]
377 fn lock_removal_works() -> anyhow::Result<()> {
378 let mut toml = "[package.metadata.hackerman.lock]\ndependencies = 1".parse()?;
379 restore_toml(&mut toml)?;
380 assert_eq!(toml.to_string(), "");
381 Ok(())
382 }
383
384 #[test]
385 fn lock_removal_works_without_lock_present() -> anyhow::Result<()> {
386 let mut toml = "".parse()?;
387 restore_toml(&mut toml)?;
388 assert_eq!(toml.to_string(), "");
389 Ok(())
390 }
391
392 #[test]
393 fn add_banner_works() -> anyhow::Result<()> {
394 let s = r#"
395[dependencies]
396version = 1.0
397
398[dev-dependencies]
399"#;
400 let mut toml = s.parse()?;
401 add_banner(&mut toml)?;
402 let expected = format!("{BANNER}{s}");
403 assert_eq!(expected, toml.to_string());
404 Ok(())
405 }
406
407 #[test]
408 fn set_dependencies_works_0() -> anyhow::Result<()> {
409 let mut toml = r#"
410[dependencies]
411package = 1.0
412"#
413 .parse::<Document>()?;
414
415 let mut feats = BTreeSet::new();
416 feats.insert("dummy".to_string());
417
418 let changes = [ChangePackage {
419 name: "package".to_string(),
420 ty: Ty::Norm,
421 version: Version::new(1, 0, 0),
422 source: PackageSource::CRATES_IO,
423 feats,
424 rename: false,
425 has_default: false,
426 }];
427
428 set_dependencies_toml(&mut toml, false, &changes)?;
429
430 let expected = r#"
431[dependencies]
432package = { version = "1.0.0", features = ["dummy"] }
433
434[package.metadata.hackerman.stash.dependencies]
435package = 1.0
436"#;
437
438 assert_eq!(toml.to_string(), expected);
439
440 Ok(())
441 }
442 }