candding 0.1.0

Pure candle embeddings for Rust with no ONNX: dense, sparse, multi-vector and rerank models today, image models on the roadmap
Documentation
//! What the hub's answer may name before anything is written: a commit of 40 lowercase hexadecimal characters, and
//! file paths that stay inside the snapshot directory.

use std::path::Component;
use std::path::Path;

use hf_hub::repository::RepoTreeEntry;

use crate::error::CanddingError;
use crate::error::Result;

/// One file of the listing, with what `verify` checks a downloaded copy against.
pub(super) struct ListedFile {
  pub path: String,
  pub size: u64,
  pub oid: String,
  /// Whether git stores the content itself, which makes `oid` its blob id; for an LFS or xet file it names a pointer.
  pub in_git: bool,
}

pub(super) fn files(entries: Vec<RepoTreeEntry>) -> Vec<ListedFile> {
  entries
    .into_iter()
    .filter_map(|entry| match entry {
      RepoTreeEntry::File {
        path,
        size,
        oid,
        lfs,
        xet_hash,
        ..
      } => Some(ListedFile {
        path,
        size,
        oid,
        in_git: lfs.is_none() && xet_hash.is_none(),
      }),
      RepoTreeEntry::Directory { .. } => None,
    })
    .collect()
}

/// The commit names the snapshot directory, so anything but 40 lowercase hexadecimal characters is refused.
pub(super) fn check_commit(id: &str, commit: &str) -> Result<()> {
  let hex = commit
    .bytes()
    .all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f'));
  if commit.len() == 40 && hex {
    return Ok(());
  }
  Err(unsafe_path(
    id,
    commit,
    "is not a commit of 40 lowercase hexadecimal characters",
  ))
}

/// Every listed file is checked, selected or not: a listing with such a path did not come from a git tree.
pub(super) fn check_paths(id: &str, files: &[ListedFile]) -> Result<()> {
  match files
    .iter()
    .find_map(|file| problem(&file.path).map(|reason| (file, reason)))
  {
    Some((file, reason)) => Err(unsafe_path(id, &file.path, reason)),
    None => Ok(()),
  }
}

/// Why `path` would not stay inside the snapshot directory; the last rule catches Windows drive prefixes.
pub(super) fn problem(path: &str) -> Option<&'static str> {
  if Path::new(path).has_root() {
    Some("is absolute")
  } else if path.contains('\\') {
    Some("contains a backslash")
  } else if path.split('/').any(str::is_empty) {
    Some("has an empty component")
  } else if path.split('/').any(|part| part == "..") {
    Some("has a `..` component")
  } else if path.split('/').any(|part| part == ".") {
    Some("has a `.` component")
  } else if !Path::new(path)
    .components()
    .all(|part| matches!(part, Component::Normal(_)))
  {
    Some("has a component that is not a plain name")
  } else {
    None
  }
}

fn unsafe_path(id: &str, path: &str, reason: &str) -> CanddingError {
  CanddingError::UnsafeHubPath {
    id: id.to_string(),
    path: path.to_string(),
    reason: reason.to_string(),
  }
}