Skip to main content

conduit_cli/core/
local_mods.rs

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