conduit_cli/core/
local_mods.rs1use crate::core::filesystem::config::ConduitConfig;
2use crate::core::error::{CoreError, CoreResult};
3use crate::core::filesystem::lock::{ConduitLock, LockedMod};
4use crate::core::paths::CorePaths;
5use crate::inspector::JarInspector;
6use sha2::{Digest, Sha256};
7use std::collections::BTreeSet;
8use std::fs;
9use std::io::Read;
10use std::path::{Path, PathBuf};
11
12#[derive(Debug, Clone)]
13pub struct AddedLocalMod {
14 pub key: String,
15 pub filename: String,
16}
17
18#[derive(Debug, Clone, Default)]
19pub struct AddLocalModsReport {
20 pub added: Vec<AddedLocalMod>,
21}
22
23pub fn add_local_mods_to_project(
24 paths: &CorePaths,
25 jar_paths: Vec<PathBuf>,
26) -> CoreResult<AddLocalModsReport> {
27 let mut config: ConduitConfig = ConduitLock::load_config(paths)?;
28 let mut lock: ConduitLock = ConduitLock::load_lock(paths)?;
29
30 fs::create_dir_all(paths.mods_dir())?;
31
32 let mut report = AddLocalModsReport::default();
33
34 for jar in jar_paths {
35 let jar = normalize_path(&paths.project_dir, &jar);
36 if !jar.exists() {
37 return Err(CoreError::Io(std::io::Error::new(
38 std::io::ErrorKind::NotFound,
39 format!("File not found: {}", jar.display()),
40 )));
41 }
42
43 let filename = jar
44 .file_name()
45 .and_then(|n| n.to_str())
46 .ok_or_else(|| {
47 CoreError::Io(std::io::Error::new(
48 std::io::ErrorKind::InvalidInput,
49 "Invalid file name",
50 ))
51 })?
52 .to_string();
53
54 let dest_path = paths.mods_dir().join(&filename);
55 if !dest_path.exists() {
56 fs::copy(&jar, &dest_path)?;
57 }
58
59 let sha256 = sha256_file(&jar)?;
60 let mod_id = JarInspector::extract_primary_mod_id(&jar).ok().flatten();
61 let key = local_key(&filename, mod_id.as_deref());
62
63 let id = format!("local:{}", &sha256[..std::cmp::min(8, sha256.len())]);
64
65 config.mods.insert(key.clone(), "local".to_string());
66 lock.locked_mods.insert(
67 key.clone(),
68 LockedMod {
69 id,
70 version_id: "local".to_string(),
71 filename: filename.clone(),
72 url: "local".to_string(),
73 hash: sha256,
74 dependencies: Vec::new(),
75 },
76 );
77
78 report.added.push(AddedLocalMod { key, filename });
79 }
80
81 ConduitLock::save_config(paths, &config)?;
82 ConduitLock::save_lock(paths, &lock)?;
83
84 Ok(report)
85}
86
87#[derive(Debug, Clone, Default)]
88pub struct MissingLocalReport {
89 pub missing_files: Vec<String>,
90 pub missing_lock_entries: Vec<String>,
91}
92
93pub fn find_missing_local_mods(paths: &CorePaths) -> CoreResult<MissingLocalReport> {
94 let config = ConduitLock::load_config(paths)?;
95 let lock = ConduitLock::load_lock(paths)?;
96
97 let mut missing_files: BTreeSet<String> = BTreeSet::new();
98 let mut missing_lock_entries: BTreeSet<String> = BTreeSet::new();
99
100 for (key, value) in &config.mods {
101 if value != "local" {
102 continue;
103 }
104
105 if let Some(locked) = lock.locked_mods.get(key) {
106 if locked.url != "local" {
107 continue;
108 }
109 let on_disk = paths.mods_dir().join(&locked.filename).exists();
110 if !on_disk {
111 missing_files.insert(locked.filename.clone());
112 }
113 } else {
114 missing_lock_entries.insert(key.clone());
115 }
116 }
117
118 Ok(MissingLocalReport {
119 missing_files: missing_files.into_iter().collect(),
120 missing_lock_entries: missing_lock_entries.into_iter().collect(),
121 })
122}
123
124fn normalize_path(project_dir: &Path, p: &Path) -> PathBuf {
125 if p.is_absolute() {
126 p.to_path_buf()
127 } else {
128 project_dir.join(p)
129 }
130}
131
132fn sha256_file(path: &Path) -> CoreResult<String> {
133 let mut file = fs::File::open(path)?;
134 let mut hasher = Sha256::new();
135 let mut buf = [0u8; 8192];
136 loop {
137 let n = file.read(&mut buf)?;
138 if n == 0 {
139 break;
140 }
141 hasher.update(&buf[..n]);
142 }
143 Ok(format!("{:x}", hasher.finalize()))
144}
145
146fn local_key(filename: &str, mod_id: Option<&str>) -> String {
147 if let Some(id) = mod_id {
148 return format!("local:{}", id.to_lowercase());
149 }
150 format!("local:{}", local_key_from_filename(filename))
151}
152
153fn local_key_from_filename(filename: &str) -> String {
154 let stem = filename.strip_suffix(".jar").unwrap_or(filename);
155 let mut out = String::new();
156 let mut last_dash = false;
157 for ch in stem.chars() {
158 let c = ch.to_ascii_lowercase();
159 let is_ok = c.is_ascii_alphanumeric();
160 if is_ok {
161 out.push(c);
162 last_dash = false;
163 } else if !last_dash {
164 out.push('-');
165 last_dash = true;
166 }
167 }
168 let trimmed = out.trim_matches('-');
169 if trimmed.is_empty() {
170 "my-local-mod".to_string()
171 } else {
172 trimmed.to_string()
173 }
174}