mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-15 11:19:07 +02:00
Lot 4 : lecteur GGUF, couches du modèle et emplacements portés en Rust
gguf.rs reprend backend_gguf.go à l'identique : en-tête, clés et table des tenseurs lus sous les mêmes bornes (chaînes, compteurs, profondeur des tableaux, alignement, premier octet du dernier tenseur), v1/v2/v3 et gros-boutiste, modèles en tranches (shardFamily porté avec), cache par empreinte taille/date, mêmes messages d'erreur. Un panic éventuel reste rattrapé en « GGUF illisible ». Tous les tests Go sont portés un pour un, avec le même constructeur de GGUF synthétique. Pour /api/model/layers : resolveModelPath / resolveServeModelPath et leurs dossiers de modèles (models/, state/model_dirs, LOKI_MODEL_DIRS), la liste stockée étant passée par l'appelant qui la lit via le store ; le corps JSON de handleModelLayers est reproduit tel quel. paths.rs sert /api/paths avec le même JSON que lokiPaths. Le « binaire en cours » reste celui du serveur Go supervisé, passé par l'appelant, comme le projet actif (state/active_project) et LOKI_WORKSPACE. Les équivalents de filepath.Clean/Join/Dir/Abs/IsAbs garantissent les mêmes chaînes que le Go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0d75e90386
commit
d805d9f1ba
2 files changed
+2032
-2
No files matched your search
+1491
-1
File diff suppressed because it is too large.
Load diff
+541
-1
@@ -1 +1,541 @@
|
||||
//! Lot 4 : module en cours de portage.
|
||||
//! Emplacements de Loki (port de `sys_paths.go`) : ce que sert
|
||||
//! `GET /api/paths` au bloc « Emplacements » de l'UI.
|
||||
//!
|
||||
//! « Où sont mes fichiers ? » — un binaire unique qui s'auto-installe rend les
|
||||
//! emplacements invisibles : on ne sait plus si le binaire lancé EST
|
||||
//! l'application ou une simple copie, ni où atterrissent la config, les
|
||||
//! modèles et les fichiers de l'agent. La réponse est centralisée ici.
|
||||
//!
|
||||
//! Ce que le Go lit dans son environnement est passé en paramètre : `$LOKI_HOME`
|
||||
//! (`home`), le binaire en cours (`go_exe` : tant que la migration n'est pas
|
||||
//! finie, « binaire en cours » désigne le serveur Go supervisé), le projet
|
||||
//! actif (bucket `state`, clé `active_project`) et `$LOKI_WORKSPACE`.
|
||||
//!
|
||||
//! Le module porte aussi les équivalents de `path/filepath` dont les ports
|
||||
//! ont besoin pour produire EXACTEMENT les mêmes chaînes que le Go
|
||||
//! (`Clean`, `Join`, `Dir`, `Abs`, `IsAbs`) et `baseName`.
|
||||
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
/// Bucket `state`, clé du slug du projet actif (`stActiveProjectKey`).
|
||||
pub const ST_ACTIVE_PROJECT_KEY: &str = "active_project";
|
||||
/// Projet par défaut quand aucun n'est enregistré (`defaultProjectSlug`).
|
||||
pub const DEFAULT_PROJECT_SLUG: &str = "generale";
|
||||
/// Variable qui impose le dossier de travail de l'agent (`workspaceEnv`).
|
||||
pub const WORKSPACE_ENV: &str = "LOKI_WORKSPACE";
|
||||
|
||||
/// Réponse de `GET /api/paths` (type `lokiPaths` du Go, mêmes noms JSON, même
|
||||
/// ordre).
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct LokiPaths {
|
||||
/// LOKI_HOME : racine des données
|
||||
#[serde(rename = "home")]
|
||||
pub home: String,
|
||||
/// loki.db : config, préférences, conversation, clés
|
||||
#[serde(rename = "database")]
|
||||
pub database: String,
|
||||
/// binaire en cours d'exécution (le serveur Go pendant la migration)
|
||||
#[serde(rename = "exe")]
|
||||
pub exe: String,
|
||||
/// binaire installé (peut différer de `exe`)
|
||||
#[serde(rename = "installed")]
|
||||
pub installed: String,
|
||||
/// dossier de travail du mode agent
|
||||
#[serde(rename = "workspace")]
|
||||
pub workspace: String,
|
||||
#[serde(rename = "memory")]
|
||||
pub memory: String,
|
||||
/// scripts durables de l'agent (hors workspace jetable)
|
||||
#[serde(rename = "scripts")]
|
||||
pub scripts: String,
|
||||
#[serde(rename = "presets")]
|
||||
pub presets: String,
|
||||
#[serde(rename = "models")]
|
||||
pub models: String,
|
||||
#[serde(rename = "backends")]
|
||||
pub backends: String,
|
||||
}
|
||||
|
||||
/// `currentPaths()` du Go.
|
||||
///
|
||||
/// - `home` : `$LOKI_HOME` (rendu tel quel, comme `LokiHome()`) ;
|
||||
/// - `go_exe` : binaire Go supervisé (équivalent de `os.Executable()`),
|
||||
/// symlinks résolus s'il existe ;
|
||||
/// - `active_project` : valeur brute de `state/active_project` (vide ou
|
||||
/// absente → projet « generale ») ;
|
||||
/// - `workspace_env` : `$LOKI_WORKSPACE`, s'il est défini.
|
||||
///
|
||||
/// Fait des entrées/sorties (création et sonde du dossier de travail, comme
|
||||
/// `agentWorkspace`) : à appeler hors du runtime async (`spawn_blocking`).
|
||||
pub fn current_paths(
|
||||
home: &Path,
|
||||
go_exe: &Path,
|
||||
active_project: &str,
|
||||
workspace_env: Option<&str>,
|
||||
) -> LokiPaths {
|
||||
let h = home.to_string_lossy().into_owned();
|
||||
let exe = eval_symlinks(go_exe).unwrap_or_else(|| go_exe.to_string_lossy().into_owned());
|
||||
LokiPaths {
|
||||
home: h.clone(),
|
||||
database: go_join(&[&h, "loki.db"]),
|
||||
exe,
|
||||
installed: installed_exe_path(home),
|
||||
workspace: agent_workspace(home, workspace_env),
|
||||
memory: project_memory_dir(home, active_project),
|
||||
scripts: go_join(&[&h, "scripts"]),
|
||||
presets: go_join(&[&h, "presets"]),
|
||||
models: go_join(&[&h, "models"]),
|
||||
backends: go_join(&[&h, "backends"]),
|
||||
}
|
||||
}
|
||||
|
||||
/// `installedExePath()` : sous Unix les installateurs posent
|
||||
/// `/usr/local/bin/loki` ; sous Windows, `LOKI_HOME\bin\loki.exe`.
|
||||
pub fn installed_exe_path(home: &Path) -> String {
|
||||
if cfg!(windows) {
|
||||
go_join(&[&home.to_string_lossy(), "bin", "loki.exe"])
|
||||
} else {
|
||||
"/usr/local/bin/loki".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// `memoryDir()` = `projectMemoryDir(activeProjectSlug())` : dossier mémoire
|
||||
/// du projet actif. `active_project` est la valeur stockée, nettoyée ici comme
|
||||
/// dans `activeProjectSlug` (espaces retirés, vide → « generale »).
|
||||
pub fn project_memory_dir(home: &Path, active_project: &str) -> String {
|
||||
let mut slug = active_project.trim();
|
||||
if slug.is_empty() {
|
||||
slug = DEFAULT_PROJECT_SLUG;
|
||||
}
|
||||
go_join(&[&home.to_string_lossy(), "memory", slug])
|
||||
}
|
||||
|
||||
/// `workspaceCandidates()` : `$LOKI_WORKSPACE`, `LOKI_HOME/workspace`,
|
||||
/// `~/loki/workspace`, puis `<tmp>/loki-workspace`.
|
||||
pub fn workspace_candidates(home: &Path, workspace_env: Option<&str>) -> Vec<String> {
|
||||
let mut c = Vec::new();
|
||||
if let Some(v) = workspace_env.map(str::trim).filter(|v| !v.is_empty()) {
|
||||
c.push(v.to_string());
|
||||
}
|
||||
c.push(go_join(&[&home.to_string_lossy(), "workspace"]));
|
||||
if let Some(h) = user_home_dir() {
|
||||
c.push(go_join(&[&h, "loki", "workspace"]));
|
||||
}
|
||||
c.push(go_join(&[
|
||||
&std::env::temp_dir().to_string_lossy(),
|
||||
"loki-workspace",
|
||||
]));
|
||||
c
|
||||
}
|
||||
|
||||
/// `agentWorkspace()` : premier candidat qu'on peut créer ET où l'on peut
|
||||
/// écrire (sonde réelle : un dossier existant peut être en lecture seule),
|
||||
/// sinon le répertoire courant. Calculé une fois par processus et par
|
||||
/// entrée, comme le `sync.Once` du Go.
|
||||
pub fn agent_workspace(home: &Path, workspace_env: Option<&str>) -> String {
|
||||
type Cache = Mutex<HashMap<(String, Option<String>), String>>;
|
||||
static CACHE: OnceLock<Cache> = OnceLock::new();
|
||||
let key = (
|
||||
home.to_string_lossy().into_owned(),
|
||||
workspace_env.map(str::to_string),
|
||||
);
|
||||
let cache = CACHE.get_or_init(Default::default);
|
||||
if let Some(v) = cache.lock().unwrap_or_else(|e| e.into_inner()).get(&key) {
|
||||
return v.clone();
|
||||
}
|
||||
let found = workspace_candidates(home, workspace_env)
|
||||
.into_iter()
|
||||
.filter(|d| !d.is_empty())
|
||||
.find(|d| mkdir_all(d).is_ok() && probe_writable(d))
|
||||
.unwrap_or_else(|| {
|
||||
// Dernier recours : le répertoire courant, comportement historique.
|
||||
std::env::current_dir()
|
||||
.map(|d| d.to_string_lossy().into_owned())
|
||||
.unwrap_or_default()
|
||||
});
|
||||
cache
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.entry(key)
|
||||
.or_insert(found)
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn mkdir_all(dir: &str) -> io::Result<()> {
|
||||
let mut b = std::fs::DirBuilder::new();
|
||||
b.recursive(true);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::DirBuilderExt;
|
||||
b.mode(0o755);
|
||||
}
|
||||
b.create(dir)
|
||||
}
|
||||
|
||||
/// Équivalent de `os.CreateTemp(dir, ".probe-*")` suivi de sa suppression.
|
||||
fn probe_writable(dir: &str) -> bool {
|
||||
for _ in 0..16 {
|
||||
let mut r = [0u8; 8];
|
||||
if getrandom::fill(&mut r).is_err() {
|
||||
return false;
|
||||
}
|
||||
let p = Path::new(dir).join(format!(".probe-{}", hex::encode(r)));
|
||||
match std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&p)
|
||||
{
|
||||
Ok(f) => {
|
||||
drop(f);
|
||||
let _ = std::fs::remove_file(&p);
|
||||
return true;
|
||||
}
|
||||
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => continue,
|
||||
Err(_) => return false,
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// `os.UserHomeDir()` : `$HOME` sous Unix, `%USERPROFILE%` sous Windows ;
|
||||
/// vide = inconnu.
|
||||
fn user_home_dir() -> Option<String> {
|
||||
let var = if cfg!(windows) { "USERPROFILE" } else { "HOME" };
|
||||
std::env::var(var).ok().filter(|h| !h.is_empty())
|
||||
}
|
||||
|
||||
/// `filepath.EvalSymlinks` : chemin résolu, ou `None` s'il n'existe pas.
|
||||
pub fn eval_symlinks(p: &Path) -> Option<String> {
|
||||
let r = std::fs::canonicalize(p).ok()?;
|
||||
let s = r.to_string_lossy().into_owned();
|
||||
// Windows : canonicalize rend un chemin « verbatim » (\\?\C:\…) que le Go
|
||||
// ne produit jamais.
|
||||
let s = if let Some(rest) = s.strip_prefix(r"\\?\UNC\") {
|
||||
format!(r"\\{rest}")
|
||||
} else if let Some(rest) = s.strip_prefix(r"\\?\") {
|
||||
rest.to_string()
|
||||
} else {
|
||||
s
|
||||
};
|
||||
Some(go_clean(&s))
|
||||
}
|
||||
|
||||
// --- équivalents de path/filepath -----------------------------------------
|
||||
|
||||
/// Séparateur natif (`filepath.Separator`).
|
||||
const SEP: u8 = if cfg!(windows) { b'\\' } else { b'/' };
|
||||
|
||||
fn is_sep(c: u8) -> bool {
|
||||
c == b'/' || (cfg!(windows) && c == b'\\')
|
||||
}
|
||||
|
||||
fn from_slash(s: &str) -> String {
|
||||
if cfg!(windows) {
|
||||
s.replace('/', "\\")
|
||||
} else {
|
||||
s.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Longueur du volume en tête du chemin (`C:` ou `\\hôte\partage`) ; toujours
|
||||
/// 0 hors Windows.
|
||||
pub fn volume_name_len(p: &str) -> usize {
|
||||
if !cfg!(windows) {
|
||||
return 0;
|
||||
}
|
||||
let b = p.as_bytes();
|
||||
let l = b.len();
|
||||
if l >= 2 && b[1] == b':' && b[0].is_ascii_alphabetic() {
|
||||
return 2;
|
||||
}
|
||||
if l >= 5 && is_sep(b[0]) && is_sep(b[1]) && !is_sep(b[2]) && b[2] != b'.' {
|
||||
let mut n = 3;
|
||||
while n < l - 1 {
|
||||
if is_sep(b[n]) {
|
||||
n += 1;
|
||||
if !is_sep(b[n]) {
|
||||
if b[n] == b'.' {
|
||||
break;
|
||||
}
|
||||
while n < l && !is_sep(b[n]) {
|
||||
n += 1;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
break;
|
||||
}
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
/// `filepath.Clean` : forme lexicale la plus courte (séparateurs répétés,
|
||||
/// `.` et `..` résolus sans toucher au disque).
|
||||
pub fn go_clean(path: &str) -> String {
|
||||
let vol_len = volume_name_len(path);
|
||||
let vol = &path[..vol_len];
|
||||
let rest = &path.as_bytes()[vol_len..];
|
||||
if rest.is_empty() {
|
||||
let b = path.as_bytes();
|
||||
if vol_len > 1 && is_sep(b[0]) && is_sep(b[1]) {
|
||||
return from_slash(path); // UNC
|
||||
}
|
||||
return format!("{path}.");
|
||||
}
|
||||
let rooted = is_sep(rest[0]);
|
||||
let n = rest.len();
|
||||
let mut out: Vec<u8> = Vec::with_capacity(n);
|
||||
let (mut r, mut dotdot) = (0usize, 0usize);
|
||||
if rooted {
|
||||
out.push(SEP);
|
||||
r = 1;
|
||||
dotdot = 1;
|
||||
}
|
||||
while r < n {
|
||||
if is_sep(rest[r]) || (rest[r] == b'.' && (r + 1 == n || is_sep(rest[r + 1]))) {
|
||||
// séparateur vide ou élément « . »
|
||||
r += 1;
|
||||
} else if rest[r] == b'.'
|
||||
&& r + 1 < n
|
||||
&& rest[r + 1] == b'.'
|
||||
&& (r + 2 == n || is_sep(rest[r + 2]))
|
||||
{
|
||||
r += 2;
|
||||
if out.len() > dotdot {
|
||||
let mut w = out.len() - 1;
|
||||
while w > dotdot && !is_sep(out[w]) {
|
||||
w -= 1;
|
||||
}
|
||||
out.truncate(w);
|
||||
} else if !rooted {
|
||||
if !out.is_empty() {
|
||||
out.push(SEP);
|
||||
}
|
||||
out.extend_from_slice(b"..");
|
||||
dotdot = out.len();
|
||||
}
|
||||
} else {
|
||||
if (rooted && out.len() != 1) || (!rooted && !out.is_empty()) {
|
||||
out.push(SEP);
|
||||
}
|
||||
while r < n && !is_sep(rest[r]) {
|
||||
out.push(rest[r]);
|
||||
r += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if out.is_empty() {
|
||||
out.push(b'.');
|
||||
}
|
||||
from_slash(&format!("{vol}{}", String::from_utf8_lossy(&out)))
|
||||
}
|
||||
|
||||
/// `filepath.Join` : éléments vides en tête ignorés, résultat nettoyé ; ""
|
||||
/// si tout est vide.
|
||||
pub fn go_join(elems: &[&str]) -> String {
|
||||
match elems.iter().position(|e| !e.is_empty()) {
|
||||
Some(i) => go_clean(&elems[i..].join(&(SEP as char).to_string())),
|
||||
None => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// `filepath.Dir` : tout sauf le dernier élément, nettoyé.
|
||||
pub fn go_dir(path: &str) -> String {
|
||||
let vol_len = volume_name_len(path);
|
||||
let b = path.as_bytes();
|
||||
let mut i = b.len();
|
||||
while i > vol_len && !is_sep(b[i - 1]) {
|
||||
i -= 1;
|
||||
}
|
||||
let dir = go_clean(&path[vol_len..i]);
|
||||
if dir == "." && vol_len > 2 {
|
||||
return path[..vol_len].to_string(); // UNC
|
||||
}
|
||||
format!("{}{dir}", &path[..vol_len])
|
||||
}
|
||||
|
||||
/// `filepath.IsAbs`.
|
||||
pub fn go_is_abs(path: &str) -> bool {
|
||||
if !cfg!(windows) {
|
||||
return path.starts_with('/');
|
||||
}
|
||||
let l = volume_name_len(path);
|
||||
if l == 0 {
|
||||
return false;
|
||||
}
|
||||
let b = path.as_bytes();
|
||||
if is_sep(b[0]) && is_sep(b[1]) {
|
||||
return true;
|
||||
}
|
||||
b.get(l).is_some_and(|&c| is_sep(c))
|
||||
}
|
||||
|
||||
/// `filepath.Abs` : chemin absolu nettoyé ; `None` si le répertoire courant
|
||||
/// est illisible.
|
||||
pub fn go_abs(path: &str) -> Option<String> {
|
||||
if go_is_abs(path) && !cfg!(windows) {
|
||||
return Some(go_clean(path));
|
||||
}
|
||||
let p = if path.is_empty() { "." } else { path };
|
||||
let abs = std::path::absolute(p).ok()?;
|
||||
Some(go_clean(&abs.to_string_lossy()))
|
||||
}
|
||||
|
||||
/// `baseName` du Go : nom de fichier d'un chemin en acceptant les DEUX
|
||||
/// séparateurs (un config.env écrit sous Windows peut être relu sous Linux).
|
||||
pub fn base_name(p: &str) -> String {
|
||||
let p = p.trim().trim_end_matches(['/', '\\']);
|
||||
match p.rfind(['/', '\\']) {
|
||||
Some(i) => p[i + 1..].to_string(),
|
||||
None => p.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Message d'une `*os.PathError` du Go : « op chemin: raison ». La raison
|
||||
/// suit la forme des `syscall.Errno` du Go sous Unix (minuscule en tête, sans
|
||||
/// le « (os error N) » de Rust).
|
||||
pub fn os_err(op: &str, path: &str, e: &io::Error) -> String {
|
||||
let mut msg = e.to_string();
|
||||
if let Some(i) = msg.rfind(" (os error ") {
|
||||
msg.truncate(i);
|
||||
}
|
||||
if cfg!(unix) {
|
||||
let mut c = msg.chars();
|
||||
if let Some(f) = c.next() {
|
||||
msg = f.to_lowercase().chain(c).collect();
|
||||
}
|
||||
}
|
||||
format!("{op} {path}: {msg}")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn json_identique_au_go() {
|
||||
let p = LokiPaths {
|
||||
home: "h".into(),
|
||||
database: "d".into(),
|
||||
exe: "e".into(),
|
||||
installed: "i".into(),
|
||||
workspace: "w".into(),
|
||||
memory: "m".into(),
|
||||
scripts: "s".into(),
|
||||
presets: "p".into(),
|
||||
models: "mo".into(),
|
||||
backends: "b".into(),
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_string(&p).unwrap(),
|
||||
r#"{"home":"h","database":"d","exe":"e","installed":"i","workspace":"w","memory":"m","scripts":"s","presets":"p","models":"mo","backends":"b"}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chemins_derives_de_home() {
|
||||
let home = tempfile::tempdir().unwrap();
|
||||
let h = home.path().to_string_lossy().into_owned();
|
||||
let exe = home.path().join("loki-go");
|
||||
std::fs::write(&exe, b"x").unwrap();
|
||||
let ws = home.path().join("ws-impose");
|
||||
let ws_s = ws.to_string_lossy().into_owned();
|
||||
let p = current_paths(home.path(), &exe, " projet-x \n", Some(&ws_s));
|
||||
assert_eq!(p.home, h);
|
||||
assert_eq!(p.database, go_join(&[&h, "loki.db"]));
|
||||
assert_eq!(p.models, go_join(&[&h, "models"]));
|
||||
assert_eq!(p.presets, go_join(&[&h, "presets"]));
|
||||
assert_eq!(p.scripts, go_join(&[&h, "scripts"]));
|
||||
assert_eq!(p.backends, go_join(&[&h, "backends"]));
|
||||
assert_eq!(p.memory, go_join(&[&h, "memory", "projet-x"]));
|
||||
assert_eq!(p.workspace, ws_s);
|
||||
assert!(ws.is_dir(), "dossier de travail non créé");
|
||||
assert_eq!(
|
||||
std::fs::read_dir(&ws).unwrap().count(),
|
||||
0,
|
||||
"sonde non supprimée"
|
||||
);
|
||||
assert!(p.exe.ends_with("loki-go"), "{}", p.exe);
|
||||
assert!(go_is_abs(&p.exe), "{}", p.exe);
|
||||
if cfg!(windows) {
|
||||
assert_eq!(p.installed, go_join(&[&h, "bin", "loki.exe"]));
|
||||
} else {
|
||||
assert_eq!(p.installed, "/usr/local/bin/loki");
|
||||
}
|
||||
// projet absent → « generale » ; binaire absent → gardé tel quel
|
||||
let absent = home.path().join("absent");
|
||||
let q = current_paths(home.path(), &absent, "", None);
|
||||
assert_eq!(q.memory, go_join(&[&h, "memory", "generale"]));
|
||||
assert_eq!(q.exe, absent.to_string_lossy());
|
||||
assert_eq!(q.workspace, go_join(&[&h, "workspace"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn candidats_dans_l_ordre() {
|
||||
let c = workspace_candidates(Path::new("/srv/loki"), Some(" "));
|
||||
assert_eq!(c[0], go_join(&["/srv/loki", "workspace"]));
|
||||
assert!(c.last().unwrap().ends_with("loki-workspace"));
|
||||
let c = workspace_candidates(Path::new("/srv/loki"), Some(" /x/ws "));
|
||||
assert_eq!(c[0], "/x/ws");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base_name_deux_separateurs() {
|
||||
assert_eq!(base_name(r"C:\models\x.gguf"), "x.gguf");
|
||||
assert_eq!(base_name("/m/y.gguf/ "), "y.gguf");
|
||||
assert_eq!(base_name("z.gguf"), "z.gguf");
|
||||
assert_eq!(base_name(""), "");
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn clean_comme_go() {
|
||||
for (i, o) in [
|
||||
("", "."),
|
||||
("abc", "abc"),
|
||||
("abc/def/", "abc/def"),
|
||||
("a//b", "a/b"),
|
||||
("/../abc", "/abc"),
|
||||
("abc/./../def", "def"),
|
||||
("../../abc", "../../abc"),
|
||||
("abc/../../..", "../.."),
|
||||
("/", "/"),
|
||||
("//abc//", "/abc"),
|
||||
] {
|
||||
assert_eq!(go_clean(i), o, "Clean({i:?})");
|
||||
}
|
||||
assert_eq!(go_dir("/a/b/c.gguf"), "/a/b");
|
||||
assert_eq!(go_dir("c.gguf"), ".");
|
||||
assert_eq!(go_dir("/c.gguf"), "/");
|
||||
assert_eq!(go_join(&["", "a", "", "b/"]), "a/b");
|
||||
assert_eq!(go_join(&["", ""]), "");
|
||||
assert!(go_is_abs("/x") && !go_is_abs("x"));
|
||||
assert_eq!(go_abs("/a/../b").as_deref(), Some("/b"));
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn clean_comme_go() {
|
||||
for (i, o) in [
|
||||
("", "."),
|
||||
("abc/def/", r"abc\def"),
|
||||
(r"C:\a\..\b", r"C:\b"),
|
||||
("C:", "C:."),
|
||||
(r"C:\", r"C:\"),
|
||||
(r"\\srv\share", r"\\srv\share"),
|
||||
(r"\\srv\share\x\..", r"\\srv\share\"),
|
||||
(r"c:/a//b/./", r"c:\a\b"),
|
||||
] {
|
||||
assert_eq!(go_clean(i), o, "Clean({i:?})");
|
||||
}
|
||||
assert_eq!(go_dir(r"C:\m\x.gguf"), r"C:\m");
|
||||
assert_eq!(go_dir(r"\\srv\share\x.gguf"), r"\\srv\share\");
|
||||
assert!(go_is_abs(r"C:\x") && go_is_abs(r"\\srv\share") && !go_is_abs(r"\x"));
|
||||
assert!(!go_is_abs("C:x"));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user