use crate::container::{Container, SectionKind};
#[derive(Debug, Clone)]
pub struct NimblePath {
pub raw: String,
pub os_hint: PathOs,
pub user_hint: Option<String>,
pub pkg_name: Option<String>,
pub pkg_version: Option<String>,
pub pkg_hash: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PathOs {
Windows,
Unix,
Unknown,
}
pub fn scan(container: &Container<'_>) -> Vec<NimblePath> {
let mut results = Vec::new();
let mut seen = std::collections::HashSet::new();
for section in container.sections() {
if section.kind != SectionKind::RoData {
continue;
}
scan_section(section.data, &mut results, &mut seen);
}
results
}
fn scan_section(
data: &[u8],
out: &mut Vec<NimblePath>,
seen: &mut std::collections::HashSet<String>,
) {
let needle = b".nimble/pkgs";
let mut start = 0;
while let Some(pos) = memchr::memmem::find(&data[start..], needle) {
let abs_pos = start + pos;
let str_start = walk_back_to_string_start(data, abs_pos);
let str_end = data[abs_pos..]
.iter()
.position(|&b| b == 0)
.map(|p| abs_pos + p)
.unwrap_or(data.len().min(abs_pos + 4096));
if let Ok(raw) = std::str::from_utf8(&data[str_start..str_end]) {
if !raw.is_empty() && seen.insert(raw.to_owned()) {
out.push(parse_nimble_path(raw));
}
}
start = str_end.saturating_add(1);
}
}
fn walk_back_to_string_start(data: &[u8], pos: usize) -> usize {
let mut i = pos;
while i > 0 {
let b = data[i - 1];
if b == 0 || !(0x20..=0x7E).contains(&b) {
return i;
}
i -= 1;
}
0
}
fn parse_nimble_path(raw: &str) -> NimblePath {
let os_hint = if raw.len() >= 3 && raw.as_bytes()[1] == b':' {
PathOs::Windows
} else if raw.starts_with('/') {
PathOs::Unix
} else {
PathOs::Unknown
};
let user_hint = extract_user_hint(raw, os_hint);
let (pkg_name, pkg_version, pkg_hash) = extract_package_info(raw);
NimblePath {
raw: raw.to_owned(),
os_hint,
user_hint,
pkg_name,
pkg_version,
pkg_hash,
}
}
fn extract_user_hint(raw: &str, os: PathOs) -> Option<String> {
match os {
PathOs::Unix => {
let after_home = raw
.strip_prefix("/home/")
.or_else(|| raw.strip_prefix("/Users/"))?;
let user = after_home.split('/').next()?;
if !user.is_empty() {
Some(user.to_owned())
} else {
None
}
}
PathOs::Windows => {
let norm = raw.replace('\\', "/");
let after = norm.find("/Users/").map(|i| &norm[i + 7..])?;
let user = after.split('/').next()?;
if !user.is_empty() {
Some(user.to_owned())
} else {
None
}
}
PathOs::Unknown => None,
}
}
fn extract_package_info(raw: &str) -> (Option<String>, Option<String>, Option<String>) {
let pkg_segment = if let Some(pos) = raw.find(".nimble/pkgs2/") {
Some((&raw[pos + 14..], true)) } else {
raw.find(".nimble/pkgs/")
.map(|pos| (&raw[pos + 13..], false))
};
let Some((segment, is_pkgs2)) = pkg_segment else {
return (None, None, None);
};
let dir_part = segment.split('/').next().unwrap_or(segment);
let parts: Vec<&str> = dir_part.splitn(2, '-').collect();
if parts.len() < 2 {
return (Some(dir_part.to_owned()), None, None);
}
let mut name_end = None;
let bytes = dir_part.as_bytes();
for i in 0..bytes.len().saturating_sub(1) {
if bytes[i] == b'-' && bytes[i + 1].is_ascii_digit() {
name_end = Some(i);
break;
}
}
let Some(ne) = name_end else {
return (Some(dir_part.to_owned()), None, None);
};
let pkg_name = &dir_part[..ne];
let remainder = &dir_part[ne + 1..];
if is_pkgs2 {
if let Some(hash_sep) = remainder.rfind('-') {
let ver = &remainder[..hash_sep];
let hash = &remainder[hash_sep + 1..];
(
Some(pkg_name.to_owned()),
Some(ver.to_owned()),
if hash.is_empty() {
None
} else {
Some(hash.to_owned())
},
)
} else {
(Some(pkg_name.to_owned()), Some(remainder.to_owned()), None)
}
} else {
(Some(pkg_name.to_owned()), Some(remainder.to_owned()), None)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_unix_pkgs2() {
let p = parse_nimble_path(
"/home/alex/.nimble/pkgs2/nimSHA2-0.1.1-6765d9a04c328c64eb56b3fa90f45690294cc8fd/nimSHA2",
);
assert_eq!(p.os_hint, PathOs::Unix);
assert_eq!(p.user_hint.as_deref(), Some("alex"));
assert_eq!(p.pkg_name.as_deref(), Some("nimSHA2"));
assert_eq!(p.pkg_version.as_deref(), Some("0.1.1"));
assert!(p.pkg_hash.is_some());
}
#[test]
fn parse_windows_pkgs2() {
let p = parse_nimble_path("C:/Users/User.name/.nimble/pkgs2/nimSHA2-0.1.1-abc123/nimSHA2");
assert_eq!(p.os_hint, PathOs::Windows);
assert_eq!(p.user_hint.as_deref(), Some("User.name"));
assert_eq!(p.pkg_name.as_deref(), Some("nimSHA2"));
}
#[test]
fn parse_legacy_pkgs() {
let p = parse_nimble_path("/home/user/.nimble/pkgs/asynctools-0.1.0/asynctools");
assert_eq!(p.pkg_name.as_deref(), Some("asynctools"));
assert_eq!(p.pkg_version.as_deref(), Some("0.1.0"));
assert_eq!(p.pkg_hash, None);
}
#[test]
fn parse_relative_nimble() {
let p = parse_nimble_path(".nimble/pkgs/foo-1.0/foo");
assert_eq!(p.os_hint, PathOs::Unknown);
assert_eq!(p.pkg_name.as_deref(), Some("foo"));
}
}