Skip to main content

conduit_cli/core/
local_mods.rs

1use 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}