use super::inode::{InodeRecord, InodeTable};
use super::resolve::{
is_hex_revision_spec, is_valid_tree_entry_name, join_path_components, reference_candidates,
reference_revision_name, revision_candidates, split_path_bytes, RefNamespace,
};
use crate::cli::SubmodulesMode;
use crate::config::MountConfig;
use crate::git::{
CommitInfo, GitObjectId, GitObjectKind, GitReference, GixRepository, HistoryEntry, LfsState,
ObjectStore, RevResolver, StoreError, TreeEntry, TreeInfo,
};
use anyhow::Result;
use fuser::{
fuse_forget_one, FileAttr, FileType, Filesystem, KernelConfig, ReplyAttr, ReplyBmap,
ReplyCreate, ReplyData, ReplyDirectory, ReplyEmpty, ReplyEntry, ReplyIoctl, ReplyLock,
ReplyLseek, ReplyOpen, ReplyStatfs, ReplyWrite, ReplyXattr, Request,
};
use libc::{
EACCES, EINVAL, EIO, EISDIR, ENAMETOOLONG, ENODATA, ENOENT, ENOSYS, ENOTDIR, ERANGE, EROFS,
O_ACCMODE, O_APPEND, O_RDWR, O_TRUNC, O_WRONLY, W_OK,
};
use std::collections::BTreeSet;
use std::convert::TryFrom;
use std::ffi::{OsStr, OsString};
use std::io::{Read, Seek, SeekFrom};
use std::os::unix::ffi::{OsStrExt, OsStringExt};
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
const BLOCK_SIZE: u32 = 4096;
const DIRECTORY_SIZE: u64 = 4096;
const LONG_TTL: Duration = Duration::from_secs(24 * 60 * 60);
const SHORT_TTL: Duration = Duration::from_secs(1);
const XATTR_OID: &[u8] = b"user.timefs.oid";
const XATTR_MODE: &[u8] = b"user.timefs.mode";
const XATTR_TYPE: &[u8] = b"user.timefs.type";
const XATTR_COMMIT: &[u8] = b"user.timefs.commit";
const XATTR_LFS: &[u8] = b"user.timefs.lfs";
const ROOT_ENTRY_NAMES: [&[u8]; 5] = [b"now", b"at", b"commits", b"refs", b"history"];
const REFS_ENTRY_NAMES: [&[u8]; 3] = [b"heads", b"tags", b"remotes"];
#[derive(Debug)]
pub(crate) struct HeadFilesystem {
store: Arc<GixRepository>,
submodules: SubmodulesMode,
uid: u32,
gid: u32,
mount_time: SystemTime,
inodes: InodeTable<NodeKey, Node>,
logged_backend_errors: Mutex<BTreeSet<String>>,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
enum NodeKey {
Root,
Path(Vec<Vec<u8>>),
}
#[derive(Clone, Debug)]
struct Node {
kind: NodeKind,
}
#[derive(Clone, Debug)]
enum NodeKind {
Root,
AtResolver,
AtPrefix,
CommitsResolver,
HistoryResolver,
HistoryPrefix,
HistoryDirectory {
versions: Arc<[HistoryVersion]>,
},
RefsRoot,
RefIndex {
namespace: RefNamespace,
components: Vec<Vec<u8>>,
},
Directory {
source: Arc<GixRepository>,
tree: TreeInfo,
context: RevisionContext,
},
File {
source: Arc<GixRepository>,
oid: GitObjectId,
mode: u32,
context: RevisionContext,
},
Symlink {
source: Arc<GixRepository>,
oid: GitObjectId,
context: RevisionContext,
},
GitlinkDirectory {
oid: GitObjectId,
context: RevisionContext,
},
GitlinkFile {
oid: GitObjectId,
context: RevisionContext,
},
}
#[derive(Clone, Debug)]
struct RevisionContext {
commit: CommitInfo,
ttl: TtlClass,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum TtlClass {
Long,
Short,
}
enum RevisionLookup {
Found(ResolvedRevision),
Missing(String),
}
enum AtPathResolution {
Revision {
consumed: usize,
resolved: ResolvedRevision,
},
PrefixDirectory,
}
struct ResolvedRevision {
source: Arc<GixRepository>,
tree: TreeInfo,
context: RevisionContext,
}
struct RecursedSubmodule {
source: Arc<GixRepository>,
tree: TreeInfo,
context: RevisionContext,
}
#[derive(Clone, Debug)]
struct HistoryVersion {
name: Vec<u8>,
entry: HistoryEntry,
}
struct DirEntrySpec {
ino: u64,
offset: i64,
kind: FileType,
name: OsString,
}
impl HeadFilesystem {
pub(crate) fn new(store: GixRepository, config: &MountConfig) -> Result<Self> {
let uid = config.uid.unwrap_or_else(current_uid);
let gid = config.gid.unwrap_or_else(current_gid);
let root = Node {
kind: NodeKind::Root,
};
Ok(Self {
store: Arc::new(store),
submodules: config.submodules,
uid,
gid,
mount_time: SystemTime::now(),
inodes: InodeTable::new(NodeKey::Root, root),
logged_backend_errors: Mutex::new(BTreeSet::new()),
})
}
fn lookup_record(
&mut self,
parent: u64,
name: &OsStr,
) -> Result<InodeRecord<NodeKey, Node>, i32> {
let parent_record = self.inodes.get(parent).cloned().ok_or(ENOENT)?;
let name_bytes = name.as_bytes();
if name_bytes.len() > 255 {
return Err(ENAMETOOLONG);
}
if name_bytes == b"." {
return self
.inodes
.remember_lookup(parent_record.key.clone(), || Ok(parent_record.node.clone()));
}
if name_bytes == b".." {
let parent_key = parent_key(&parent_record.key);
let resolved = self.resolve_node(&parent_key);
return self.inodes.remember_lookup(parent_key, || resolved);
}
if !is_directory_like(&parent_record.node.kind) {
return Err(ENOTDIR);
}
let child_key = child_key(&parent_record.key, name_bytes);
let resolved = self.resolve_node(&child_key);
self.inodes.remember_lookup(child_key, || resolved)
}
fn resolve_node(&self, key: &NodeKey) -> Result<Node, i32> {
match key {
NodeKey::Root => Ok(Node {
kind: NodeKind::Root,
}),
NodeKey::Path(components) => self.resolve_path(components),
}
}
fn resolve_path(&self, components: &[Vec<u8>]) -> Result<Node, i32> {
let Some(first) = components.first() else {
return Err(ENOENT);
};
match first.as_slice() {
b"now" => self.resolve_now_path(&components[1..]),
b"at" => self.resolve_at_path(&components[1..]),
b"commits" => self.resolve_commits_path(&components[1..]),
b"refs" => self.resolve_refs_path(&components[1..]),
b"history" => self.resolve_history_path(&components[1..]),
_ => Err(ENOENT),
}
}
fn resolve_now_path(&self, tail: &[Vec<u8>]) -> Result<Node, i32> {
let resolved = match self.try_resolve_revision(b"HEAD", TtlClass::Short)? {
RevisionLookup::Found(resolved) => resolved,
RevisionLookup::Missing(reason) => {
tracing::info!(revision = "HEAD", cause = %reason, "failed to resolve revision");
return Err(ENOENT);
}
};
self.resolve_revision_tree_path(resolved, tail)
}
fn resolve_at_path(&self, tail: &[Vec<u8>]) -> Result<Node, i32> {
if tail.is_empty() {
return Ok(Node {
kind: NodeKind::AtResolver,
});
}
match self.find_at_path_resolution(tail)? {
AtPathResolution::Revision { consumed, resolved } => {
self.resolve_revision_tree_path(resolved, &tail[consumed..])
}
AtPathResolution::PrefixDirectory => Ok(Node {
kind: NodeKind::AtPrefix,
}),
}
}
fn find_at_path_resolution(&self, tail: &[Vec<u8>]) -> Result<AtPathResolution, i32> {
let mut full_reason = None;
for candidate in revision_candidates(tail) {
match self
.try_resolve_revision(&candidate.spec, ttl_for_revision_spec(&candidate.spec))?
{
RevisionLookup::Found(resolved) => {
return Ok(AtPathResolution::Revision {
consumed: candidate.consumed,
resolved,
});
}
RevisionLookup::Missing(reason) => {
if candidate.consumed == tail.len() {
full_reason = Some(reason);
}
}
}
}
if self.has_at_revision_prefix(tail)? {
return Ok(AtPathResolution::PrefixDirectory);
}
let spec = join_path_components(tail);
tracing::info!(
revision = %display_bytes(&spec),
cause = %full_reason.unwrap_or_else(|| String::from("revision not found")),
"failed to resolve revision"
);
Err(ENOENT)
}
fn resolve_commits_path(&self, tail: &[Vec<u8>]) -> Result<Node, i32> {
if tail.is_empty() {
return Ok(Node {
kind: NodeKind::CommitsResolver,
});
}
let spec = tail[0].as_slice();
let resolved = match self.try_resolve_commit_prefix(spec)? {
RevisionLookup::Found(resolved) => resolved,
RevisionLookup::Missing(reason) => {
tracing::info!(
revision = %display_bytes(spec),
cause = %reason,
"failed to resolve revision"
);
return Err(ENOENT);
}
};
self.resolve_revision_tree_path(resolved, &tail[1..])
}
fn resolve_refs_path(&self, tail: &[Vec<u8>]) -> Result<Node, i32> {
if tail.is_empty() {
return Ok(Node {
kind: NodeKind::RefsRoot,
});
}
let Some(namespace) = RefNamespace::from_component(tail[0].as_slice()) else {
return Err(ENOENT);
};
self.resolve_ref_namespace_path(namespace, &tail[1..])
}
fn resolve_history_path(&self, tail: &[Vec<u8>]) -> Result<Node, i32> {
if tail.is_empty() {
return Ok(Node {
kind: NodeKind::HistoryResolver,
});
}
for candidate in revision_candidates(tail) {
let entries = self
.store
.history_entries(&candidate.spec)
.map_err(|error| self.store_error_errno(error))?;
if entries.is_empty() {
continue;
}
let versions = history_versions(&entries);
if candidate.consumed == tail.len() {
return Ok(Node {
kind: NodeKind::HistoryDirectory { versions },
});
}
if candidate.consumed.saturating_add(1) != tail.len() {
return Err(ENOTDIR);
}
let Some(name) = tail.get(candidate.consumed) else {
return Err(ENOENT);
};
let Some(version) = versions.iter().find(|version| version.name == *name) else {
return Err(ENOENT);
};
return self.resolve_history_version(version);
}
let prefix = join_path_components(tail);
let has_prefix = self
.store
.history_prefix_exists(&prefix)
.map_err(|error| self.store_error_errno(error))?;
if has_prefix {
return Ok(Node {
kind: NodeKind::HistoryPrefix,
});
}
Err(ENOENT)
}
fn resolve_ref_namespace_path(
&self,
namespace: RefNamespace,
tail: &[Vec<u8>],
) -> Result<Node, i32> {
if tail.is_empty() {
return Ok(Node {
kind: NodeKind::RefIndex {
namespace,
components: Vec::new(),
},
});
}
for candidate in reference_candidates(namespace, tail) {
if !self.has_exact_reference(&candidate.spec)? {
continue;
}
let resolved = self.resolve_existing_reference(&candidate.spec)?;
return self.resolve_revision_tree_path(resolved, &tail[candidate.consumed..]);
}
if self.has_reference_prefix(namespace, tail)? {
return Ok(Node {
kind: NodeKind::RefIndex {
namespace,
components: tail.to_vec(),
},
});
}
Err(ENOENT)
}
fn resolve_history_version(&self, version: &HistoryVersion) -> Result<Node, i32> {
let tree = self
.store
.root_tree(&version.entry.commit)
.map_err(|error| self.store_error_errno(error))?;
let context = RevisionContext {
commit: version.entry.commit.clone(),
ttl: TtlClass::Long,
};
let path = split_path_bytes(&version.entry.path);
if path.iter().any(Vec::is_empty) {
return Err(EIO);
}
self.resolve_tree_path(Arc::clone(&self.store), &context, &tree, &[], &path)
}
fn resolve_existing_reference(&self, full_name: &[u8]) -> Result<ResolvedRevision, i32> {
match self.try_resolve_revision(full_name, TtlClass::Short)? {
RevisionLookup::Found(resolved) => Ok(resolved),
RevisionLookup::Missing(reason) => {
tracing::warn!(
reference = %display_bytes(full_name),
cause = %reason,
"failed to resolve existing reference"
);
Err(EIO)
}
}
}
fn resolve_revision_tree_path(
&self,
resolved: ResolvedRevision,
tail: &[Vec<u8>],
) -> Result<Node, i32> {
if tail.is_empty() {
return Ok(Node {
kind: NodeKind::Directory {
source: Arc::clone(&resolved.source),
tree: resolved.tree,
context: resolved.context,
},
});
}
self.resolve_tree_path(
Arc::clone(&resolved.source),
&resolved.context,
&resolved.tree,
&[],
tail,
)
}
fn resolve_tree_path(
&self,
source: Arc<GixRepository>,
context: &RevisionContext,
root_tree: &TreeInfo,
relative_path: &[Vec<u8>],
components: &[Vec<u8>],
) -> Result<Node, i32> {
let mut current_source = source;
let mut current_tree = root_tree.clone();
let mut current_context = context.clone();
let mut current_relative = relative_path.to_vec();
for (index, component) in components.iter().enumerate() {
let last = index + 1 == components.len();
let entry = self
.find_entry(¤t_source, ¤t_tree, component)?
.ok_or(ENOENT)?;
let mut entry_relative = current_relative.clone();
entry_relative.push(entry.name.clone());
match entry.kind {
GitObjectKind::Tree => {
if last {
return Ok(node_from_entry(
Arc::clone(¤t_source),
&entry,
current_context.clone(),
));
}
current_tree = TreeInfo {
oid: entry.oid.clone(),
};
current_relative = entry_relative;
}
GitObjectKind::Blob => {
if last {
return Ok(node_from_entry(
Arc::clone(¤t_source),
&entry,
current_context.clone(),
));
}
return Err(ENOTDIR);
}
GitObjectKind::Commit => {
if last {
if let Some(recursed) = self.try_open_submodule(
¤t_source,
&entry_relative,
&entry.oid,
¤t_context,
)? {
return Ok(Node {
kind: NodeKind::Directory {
source: recursed.source,
tree: recursed.tree,
context: recursed.context,
},
});
}
return Ok(Node {
kind: NodeKind::GitlinkDirectory {
oid: entry.oid.clone(),
context: current_context.clone(),
},
});
}
let Some(recursed) = self.try_open_submodule(
¤t_source,
&entry_relative,
&entry.oid,
¤t_context,
)?
else {
if components[index + 1].as_slice() == b".gitlink" {
if index + 2 != components.len() {
return Err(ENOTDIR);
}
return Ok(Node {
kind: NodeKind::GitlinkFile {
oid: entry.oid.clone(),
context: current_context,
},
});
}
return Err(ENOENT);
};
current_source = recursed.source;
current_tree = recursed.tree;
current_context = recursed.context;
current_relative.clear();
}
}
}
Err(ENOENT)
}
fn try_resolve_revision(&self, spec: &[u8], ttl: TtlClass) -> Result<RevisionLookup, i32> {
let commit = match self.store.resolve_revision(spec) {
Ok(commit) => commit,
Err(error) => {
return classify_revision_error(self, spec, error);
}
};
let tree = self
.store
.root_tree(&commit)
.map_err(|error| self.store_error_errno(error))?;
Ok(RevisionLookup::Found(ResolvedRevision {
source: Arc::clone(&self.store),
tree,
context: RevisionContext { commit, ttl },
}))
}
fn try_resolve_commit_prefix(&self, spec: &[u8]) -> Result<RevisionLookup, i32> {
let commit = match self.store.resolve_commit_prefix(spec) {
Ok(commit) => commit,
Err(error) => {
return classify_revision_error(self, spec, error);
}
};
let tree = self
.store
.root_tree(&commit)
.map_err(|error| self.store_error_errno(error))?;
Ok(RevisionLookup::Found(ResolvedRevision {
source: Arc::clone(&self.store),
tree,
context: RevisionContext {
commit,
ttl: TtlClass::Long,
},
}))
}
fn find_entry(
&self,
source: &Arc<GixRepository>,
tree: &TreeInfo,
name: &[u8],
) -> Result<Option<TreeEntry>, i32> {
let entries = self.tree_entries(source, tree)?;
Ok(entries.into_iter().find(|entry| entry.name == name))
}
fn tree_entries(
&self,
source: &Arc<GixRepository>,
tree: &TreeInfo,
) -> Result<Vec<TreeEntry>, i32> {
let entries = source
.list_tree(tree)
.map_err(|error| self.store_error_errno(error))?;
for entry in &entries {
validate_tree_entry_name(&entry.name)?;
}
Ok(entries)
}
fn try_open_submodule(
&self,
source: &Arc<GixRepository>,
relative_path: &[Vec<u8>],
commit: &GitObjectId,
parent_context: &RevisionContext,
) -> Result<Option<RecursedSubmodule>, i32> {
if self.submodules != SubmodulesMode::Recurse {
return Ok(None);
}
let path = join_path_components(relative_path);
let opened = source
.open_submodule(&path, commit)
.map_err(|error| self.store_error_errno(error))?;
Ok(opened.map(|opened| RecursedSubmodule {
source: Arc::new(opened.store),
tree: opened.tree,
context: RevisionContext {
commit: opened.commit,
ttl: parent_context.ttl,
},
}))
}
fn listed_references(&self) -> Result<Vec<GitReference>, i32> {
self.store
.list_references()
.map_err(|error| self.store_error_errno(error))
}
fn has_exact_reference(&self, full_name: &[u8]) -> Result<bool, i32> {
Ok(self
.listed_references()?
.into_iter()
.any(|reference| reference.full_name == full_name))
}
fn has_reference_prefix(
&self,
namespace: RefNamespace,
components: &[Vec<u8>],
) -> Result<bool, i32> {
let relative = join_path_components(components);
let prefix = namespace.prefix_with_relative(&relative);
Ok(self
.listed_references()?
.into_iter()
.any(|reference| reference.full_name.starts_with(&prefix)))
}
fn next_reference_components(
&self,
namespace: RefNamespace,
components: &[Vec<u8>],
) -> Result<Vec<Vec<u8>>, i32> {
let relative = join_path_components(components);
let full_prefix = namespace.full_name(&relative);
let next_prefix = if relative.is_empty() {
namespace.prefix().to_vec()
} else {
let mut prefix = full_prefix;
prefix.push(b'/');
prefix
};
let mut names = BTreeSet::new();
for reference in self.listed_references()? {
let Some(remainder) = reference.full_name.strip_prefix(next_prefix.as_slice()) else {
continue;
};
if remainder.is_empty() {
continue;
}
let name = remainder
.split(|byte| *byte == b'/')
.next()
.map_or_else(Vec::new, |component| component.to_vec());
if !name.is_empty() {
names.insert(name);
}
}
Ok(names.into_iter().collect())
}
fn has_at_revision_prefix(&self, components: &[Vec<u8>]) -> Result<bool, i32> {
let prefix = join_path_components(components);
let mut slash_prefix = prefix.clone();
slash_prefix.push(b'/');
Ok(self.listed_references()?.into_iter().any(|reference| {
reference_revision_name(&reference).is_some_and(|name| name.starts_with(&slash_prefix))
}))
}
fn file_attr(&self, record: &InodeRecord<NodeKey, Node>) -> Result<FileAttr, i32> {
let size = self.node_size(&record.node)?;
Ok(FileAttr {
ino: record.ino,
size,
blocks: size_to_blocks(size),
atime: self.node_timestamp(&record.node),
mtime: self.node_timestamp(&record.node),
ctime: self.node_timestamp(&record.node),
crtime: self.node_timestamp(&record.node),
kind: node_file_type(&record.node),
perm: node_permissions(&record.node),
nlink: 1,
uid: self.uid,
gid: self.gid,
rdev: 0,
blksize: BLOCK_SIZE,
flags: 0,
})
}
fn node_timestamp(&self, node: &Node) -> SystemTime {
match &node.kind {
NodeKind::Root
| NodeKind::AtResolver
| NodeKind::AtPrefix
| NodeKind::CommitsResolver
| NodeKind::HistoryResolver
| NodeKind::HistoryPrefix
| NodeKind::HistoryDirectory { .. }
| NodeKind::RefsRoot
| NodeKind::RefIndex { .. } => self.mount_time,
NodeKind::Directory { context, .. }
| NodeKind::File { context, .. }
| NodeKind::Symlink { context, .. }
| NodeKind::GitlinkDirectory { context, .. }
| NodeKind::GitlinkFile { context, .. } => {
commit_time_to_system_time(context.commit.committer_time_seconds)
}
}
}
fn node_ttl(&self, node: &Node) -> Duration {
match &node.kind {
NodeKind::Root
| NodeKind::AtResolver
| NodeKind::AtPrefix
| NodeKind::CommitsResolver
| NodeKind::HistoryResolver
| NodeKind::HistoryPrefix
| NodeKind::HistoryDirectory { .. }
| NodeKind::RefsRoot
| NodeKind::RefIndex { .. } => SHORT_TTL,
NodeKind::Directory { context, .. }
| NodeKind::File { context, .. }
| NodeKind::Symlink { context, .. }
| NodeKind::GitlinkDirectory { context, .. }
| NodeKind::GitlinkFile { context, .. } => match context.ttl {
TtlClass::Long => LONG_TTL,
TtlClass::Short => SHORT_TTL,
},
}
}
fn node_size(&self, node: &Node) -> Result<u64, i32> {
match &node.kind {
NodeKind::Root
| NodeKind::AtResolver
| NodeKind::AtPrefix
| NodeKind::CommitsResolver
| NodeKind::HistoryResolver
| NodeKind::HistoryPrefix
| NodeKind::HistoryDirectory { .. }
| NodeKind::RefsRoot
| NodeKind::RefIndex { .. }
| NodeKind::Directory { .. }
| NodeKind::GitlinkDirectory { .. } => Ok(DIRECTORY_SIZE),
NodeKind::File { source, oid, .. } => self.blob_size(source, oid),
NodeKind::Symlink { source, oid, .. } => self
.read_symlink_target(source, oid)
.map(|target| u64::try_from(target.len()).unwrap_or(u64::MAX)),
NodeKind::GitlinkFile { oid, .. } => Ok(u64::try_from(oid.as_hex().len())
.unwrap_or(0)
.saturating_add(1)),
}
}
fn blob_size(&self, source: &Arc<GixRepository>, oid: &GitObjectId) -> Result<u64, i32> {
source
.blob_len(oid)
.map_err(|error| self.store_error_errno(error))
}
fn read_bytes(&self, node: &Node, offset: i64, size: u32) -> Result<Vec<u8>, i32> {
let start = u64::try_from(offset).map_err(|_| EINVAL)?;
let limit = usize::try_from(size).map_err(|_| EIO)?;
match &node.kind {
NodeKind::Root
| NodeKind::AtResolver
| NodeKind::AtPrefix
| NodeKind::CommitsResolver
| NodeKind::HistoryResolver
| NodeKind::HistoryPrefix
| NodeKind::HistoryDirectory { .. }
| NodeKind::RefsRoot
| NodeKind::RefIndex { .. }
| NodeKind::Directory { .. }
| NodeKind::GitlinkDirectory { .. } => Err(EISDIR),
NodeKind::Symlink { .. } => Err(EIO),
NodeKind::File { source, oid, .. } => {
let mut reader = source
.open_blob(oid)
.map_err(|error| self.store_error_errno(error))?;
reader.seek(SeekFrom::Start(start)).map_err(|_| EIO)?;
read_limited(&mut reader, limit)
}
NodeKind::GitlinkFile { oid, .. } => {
let contents = gitlink_contents(oid);
let start = usize::try_from(start).map_err(|_| EIO)?;
let end = start.saturating_add(limit).min(contents.len());
Ok(contents
.get(start..end)
.map_or_else(Vec::new, |slice| slice.to_vec()))
}
}
}
fn readlink_bytes(&self, node: &Node) -> Result<Vec<u8>, i32> {
match &node.kind {
NodeKind::Symlink { source, oid, .. } => self.read_symlink_target(source, oid),
_ => Err(EINVAL),
}
}
fn read_symlink_target(
&self,
source: &Arc<GixRepository>,
oid: &GitObjectId,
) -> Result<Vec<u8>, i32> {
source
.read_symlink_target(oid)
.map_err(|error| self.store_error_errno(error))
}
fn node_object_id<'a>(&self, node: &'a Node) -> Option<&'a GitObjectId> {
match &node.kind {
NodeKind::Root
| NodeKind::AtResolver
| NodeKind::AtPrefix
| NodeKind::CommitsResolver
| NodeKind::HistoryResolver
| NodeKind::HistoryPrefix
| NodeKind::HistoryDirectory { .. }
| NodeKind::RefsRoot
| NodeKind::RefIndex { .. } => None,
NodeKind::Directory { tree, .. } => Some(&tree.oid),
NodeKind::File { oid, .. }
| NodeKind::Symlink { oid, .. }
| NodeKind::GitlinkDirectory { oid, .. }
| NodeKind::GitlinkFile { oid, .. } => Some(oid),
}
}
fn node_revision_commit<'a>(&self, node: &'a Node) -> Option<&'a GitObjectId> {
match &node.kind {
NodeKind::Root
| NodeKind::AtResolver
| NodeKind::AtPrefix
| NodeKind::CommitsResolver
| NodeKind::HistoryResolver
| NodeKind::HistoryPrefix
| NodeKind::HistoryDirectory { .. }
| NodeKind::RefsRoot
| NodeKind::RefIndex { .. } => None,
NodeKind::Directory { context, .. }
| NodeKind::File { context, .. }
| NodeKind::Symlink { context, .. }
| NodeKind::GitlinkDirectory { context, .. }
| NodeKind::GitlinkFile { context, .. } => Some(&context.commit.oid),
}
}
fn node_git_mode(&self, node: &Node) -> Option<u32> {
match &node.kind {
NodeKind::File { mode, .. } => Some(*mode),
NodeKind::Symlink { .. } => Some(0o120000),
NodeKind::Root
| NodeKind::AtResolver
| NodeKind::AtPrefix
| NodeKind::CommitsResolver
| NodeKind::HistoryResolver
| NodeKind::HistoryPrefix
| NodeKind::HistoryDirectory { .. }
| NodeKind::RefsRoot
| NodeKind::RefIndex { .. }
| NodeKind::Directory { .. }
| NodeKind::GitlinkDirectory { .. }
| NodeKind::GitlinkFile { .. } => None,
}
}
fn node_git_type(&self, node: &Node) -> &'static str {
match node.kind {
NodeKind::File { .. } => "blob",
NodeKind::Symlink { .. } => "symlink",
NodeKind::GitlinkDirectory { .. } | NodeKind::GitlinkFile { .. } => "gitlink",
NodeKind::Root
| NodeKind::AtResolver
| NodeKind::AtPrefix
| NodeKind::CommitsResolver
| NodeKind::HistoryResolver
| NodeKind::HistoryPrefix
| NodeKind::HistoryDirectory { .. }
| NodeKind::RefsRoot
| NodeKind::RefIndex { .. }
| NodeKind::Directory { .. } => "tree",
}
}
fn node_lfs_state(&self, node: &Node) -> Result<Option<LfsState>, i32> {
match &node.kind {
NodeKind::File { source, oid, .. } => source
.lfs_state(oid)
.map_err(|error| self.store_error_errno(error)),
_ => Ok(None),
}
}
fn xattr_value(&self, node: &Node, name: &[u8]) -> Result<Vec<u8>, i32> {
match name {
XATTR_OID => self
.node_object_id(node)
.map(|oid| oid.as_hex().as_bytes().to_vec())
.ok_or(ENODATA),
XATTR_MODE => self.node_git_mode(node).map(format_git_mode).ok_or(ENODATA),
XATTR_TYPE => Ok(self.node_git_type(node).as_bytes().to_vec()),
XATTR_COMMIT => self
.node_revision_commit(node)
.map(|oid| oid.as_hex().as_bytes().to_vec())
.ok_or(ENODATA),
XATTR_LFS => match self.node_lfs_state(node)? {
Some(LfsState::Pointer) => Ok(b"pointer".to_vec()),
Some(LfsState::Resolved) => Ok(b"resolved".to_vec()),
None => Err(ENODATA),
},
_ => Err(ENODATA),
}
}
fn list_xattr_names(&self, node: &Node) -> Result<Vec<u8>, i32> {
let mut names = Vec::new();
if self.node_object_id(node).is_some() {
names.extend_from_slice(XATTR_OID);
names.push(0);
}
if self.node_git_mode(node).is_some() {
names.extend_from_slice(XATTR_MODE);
names.push(0);
}
names.extend_from_slice(XATTR_TYPE);
names.push(0);
if self.node_revision_commit(node).is_some() {
names.extend_from_slice(XATTR_COMMIT);
names.push(0);
}
if self.node_lfs_state(node)?.is_some() {
names.extend_from_slice(XATTR_LFS);
names.push(0);
}
Ok(names)
}
fn reply_xattr_bytes(&self, reply: ReplyXattr, data: &[u8], size: u32) {
let requested = match usize::try_from(size) {
Ok(requested) => requested,
Err(_) => {
reply.error(ERANGE);
return;
}
};
if size == 0 {
match u32::try_from(data.len()) {
Ok(length) => reply.size(length),
Err(_) => reply.error(ERANGE),
}
return;
}
if requested < data.len() {
reply.error(ERANGE);
return;
}
reply.data(data);
}
fn store_error_errno(&self, error: StoreError) -> i32 {
let message = error.to_string();
let should_log = match self.logged_backend_errors.lock() {
Ok(mut logged) => logged.insert(message.clone()),
Err(poisoned) => poisoned.into_inner().insert(message.clone()),
};
if should_log {
tracing::warn!(cause = %message, "filesystem request failed against the Git backend");
}
EIO
}
fn readdir_entries(&mut self, ino: u64) -> Result<Vec<DirEntrySpec>, i32> {
let record = self.inodes.get(ino).cloned().ok_or(ENOENT)?;
let parent_key = parent_key(&record.key);
let mut entries = Vec::new();
entries.push(DirEntrySpec {
ino: record.ino,
offset: 1,
kind: FileType::Directory,
name: OsString::from("."),
});
let resolved_parent = self.resolve_node(&parent_key);
let parent = self.inodes.remember_seen(parent_key, || resolved_parent)?;
entries.push(DirEntrySpec {
ino: parent.ino,
offset: 2,
kind: FileType::Directory,
name: OsString::from(".."),
});
match &record.node.kind {
NodeKind::Root => {
self.push_named_children(&record.key, &ROOT_ENTRY_NAMES, 3, &mut entries)?;
}
NodeKind::AtResolver
| NodeKind::AtPrefix
| NodeKind::CommitsResolver
| NodeKind::HistoryResolver
| NodeKind::HistoryPrefix => {}
NodeKind::HistoryDirectory { versions } => {
let mut offset = 3_i64;
for version in versions.iter() {
let key = child_key(&record.key, &version.name);
let resolved_child = self.resolve_node(&key);
let child = self.inodes.remember_seen(key, || resolved_child)?;
entries.push(DirEntrySpec {
ino: child.ino,
offset,
kind: node_file_type(&child.node),
name: OsString::from_vec(version.name.clone()),
});
offset = offset.saturating_add(1);
}
}
NodeKind::RefsRoot => {
self.push_named_children(&record.key, &REFS_ENTRY_NAMES, 3, &mut entries)?;
}
NodeKind::RefIndex {
namespace,
components,
} => {
let names = self.next_reference_components(*namespace, components)?;
let mut offset = 3_i64;
for name in names {
let key = child_key(&record.key, &name);
let resolved_child = self.resolve_node(&key);
let child = self.inodes.remember_seen(key, || resolved_child)?;
entries.push(DirEntrySpec {
ino: child.ino,
offset,
kind: node_file_type(&child.node),
name: OsString::from_vec(name),
});
offset = offset.saturating_add(1);
}
}
NodeKind::Directory { source, tree, .. } => {
let mut offset = 3_i64;
for tree_entry in self.tree_entries(source, tree)? {
let key = child_key(&record.key, &tree_entry.name);
let resolved_child = self.resolve_node(&key);
let child = self.inodes.remember_seen(key, || resolved_child)?;
entries.push(DirEntrySpec {
ino: child.ino,
offset,
kind: node_file_type(&child.node),
name: OsString::from_vec(tree_entry.name),
});
offset = offset.saturating_add(1);
}
}
NodeKind::GitlinkDirectory { .. } => {
let key = child_key(&record.key, b".gitlink");
let resolved_child = self.resolve_node(&key);
let child = self.inodes.remember_seen(key, || resolved_child)?;
entries.push(DirEntrySpec {
ino: child.ino,
offset: 3,
kind: node_file_type(&child.node),
name: OsString::from(".gitlink"),
});
}
NodeKind::File { .. } | NodeKind::Symlink { .. } | NodeKind::GitlinkFile { .. } => {
return Err(ENOTDIR);
}
}
self.inodes.evict_unreferenced();
Ok(entries)
}
fn push_named_children(
&mut self,
parent_key: &NodeKey,
names: &[&[u8]],
start_offset: i64,
entries: &mut Vec<DirEntrySpec>,
) -> Result<(), i32> {
let mut offset = start_offset;
for name in names {
let key = child_key(parent_key, name);
let resolved_child = self.resolve_node(&key);
let child = self.inodes.remember_seen(key, || resolved_child)?;
entries.push(DirEntrySpec {
ino: child.ino,
offset,
kind: node_file_type(&child.node),
name: OsString::from_vec(name.to_vec()),
});
offset = offset.saturating_add(1);
}
Ok(())
}
}
impl Filesystem for HeadFilesystem {
fn init(&mut self, _req: &Request<'_>, _config: &mut KernelConfig) -> Result<(), i32> {
Ok(())
}
fn lookup(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEntry) {
match self.lookup_record(parent, name).and_then(|record| {
let attr = self.file_attr(&record)?;
let ttl = self.node_ttl(&record.node);
Ok((record.generation, ttl, attr))
}) {
Ok((generation, ttl, attr)) => reply.entry(&ttl, &attr, generation),
Err(errno) => reply.error(errno),
}
}
fn forget(&mut self, _req: &Request<'_>, ino: u64, nlookup: u64) {
self.inodes.forget(ino, nlookup);
}
fn batch_forget(&mut self, _req: &Request<'_>, nodes: &[fuse_forget_one]) {
for node in nodes {
self.inodes.forget(node.nodeid, node.nlookup);
}
}
fn getattr(&mut self, _req: &Request<'_>, ino: u64, _fh: Option<u64>, reply: ReplyAttr) {
match self.inodes.get(ino).ok_or(ENOENT).and_then(|record| {
let attr = self.file_attr(record)?;
let ttl = self.node_ttl(&record.node);
Ok((ttl, attr))
}) {
Ok((ttl, attr)) => reply.attr(&ttl, &attr),
Err(errno) => reply.error(errno),
}
}
fn setattr(
&mut self,
_req: &Request<'_>,
_ino: u64,
_mode: Option<u32>,
_uid: Option<u32>,
_gid: Option<u32>,
_size: Option<u64>,
_atime: Option<fuser::TimeOrNow>,
_mtime: Option<fuser::TimeOrNow>,
_ctime: Option<SystemTime>,
_fh: Option<u64>,
_crtime: Option<SystemTime>,
_chgtime: Option<SystemTime>,
_bkuptime: Option<SystemTime>,
_flags: Option<u32>,
reply: ReplyAttr,
) {
reply.error(EROFS);
}
fn readlink(&mut self, _req: &Request<'_>, ino: u64, reply: ReplyData) {
match self
.inodes
.get(ino)
.ok_or(ENOENT)
.and_then(|record| self.readlink_bytes(&record.node))
{
Ok(data) => reply.data(&data),
Err(errno) => reply.error(errno),
}
}
fn mknod(
&mut self,
_req: &Request<'_>,
_parent: u64,
_name: &OsStr,
_mode: u32,
_umask: u32,
_rdev: u32,
reply: ReplyEntry,
) {
reply.error(EROFS);
}
fn mkdir(
&mut self,
_req: &Request<'_>,
_parent: u64,
_name: &OsStr,
_mode: u32,
_umask: u32,
reply: ReplyEntry,
) {
reply.error(EROFS);
}
fn unlink(&mut self, _req: &Request<'_>, _parent: u64, _name: &OsStr, reply: ReplyEmpty) {
reply.error(EROFS);
}
fn rmdir(&mut self, _req: &Request<'_>, _parent: u64, _name: &OsStr, reply: ReplyEmpty) {
reply.error(EROFS);
}
fn symlink(
&mut self,
_req: &Request<'_>,
_parent: u64,
_link_name: &OsStr,
_target: &Path,
reply: ReplyEntry,
) {
reply.error(EROFS);
}
fn rename(
&mut self,
_req: &Request<'_>,
_parent: u64,
_name: &OsStr,
_newparent: u64,
_newname: &OsStr,
_flags: u32,
reply: ReplyEmpty,
) {
reply.error(EROFS);
}
fn link(
&mut self,
_req: &Request<'_>,
_ino: u64,
_newparent: u64,
_newname: &OsStr,
reply: ReplyEntry,
) {
reply.error(EROFS);
}
fn open(&mut self, _req: &Request<'_>, ino: u64, flags: i32, reply: ReplyOpen) {
let access_mode = flags & O_ACCMODE;
if access_mode == O_WRONLY || access_mode == O_RDWR || flags & (O_APPEND | O_TRUNC) != 0 {
reply.error(EACCES);
return;
}
match self.inodes.get(ino).map(|record| &record.node.kind) {
Some(NodeKind::Root)
| Some(NodeKind::AtResolver)
| Some(NodeKind::AtPrefix)
| Some(NodeKind::CommitsResolver)
| Some(NodeKind::HistoryResolver)
| Some(NodeKind::HistoryPrefix)
| Some(NodeKind::HistoryDirectory { .. })
| Some(NodeKind::RefsRoot)
| Some(NodeKind::RefIndex { .. })
| Some(NodeKind::Directory { .. })
| Some(NodeKind::GitlinkDirectory { .. }) => reply.error(EISDIR),
Some(NodeKind::File { .. }) | Some(NodeKind::GitlinkFile { .. }) => reply.opened(0, 0),
Some(NodeKind::Symlink { .. }) => reply.error(EACCES),
None => reply.error(ENOENT),
}
}
fn read(
&mut self,
_req: &Request<'_>,
ino: u64,
_fh: u64,
offset: i64,
size: u32,
_flags: i32,
_lock_owner: Option<u64>,
reply: ReplyData,
) {
match self
.inodes
.get(ino)
.ok_or(ENOENT)
.and_then(|record| self.read_bytes(&record.node, offset, size))
{
Ok(data) => reply.data(&data),
Err(errno) => reply.error(errno),
}
}
fn write(
&mut self,
_req: &Request<'_>,
_ino: u64,
_fh: u64,
_offset: i64,
_data: &[u8],
_write_flags: u32,
_flags: i32,
_lock_owner: Option<u64>,
reply: ReplyWrite,
) {
reply.error(EROFS);
}
fn flush(
&mut self,
_req: &Request<'_>,
_ino: u64,
_fh: u64,
_lock_owner: u64,
reply: ReplyEmpty,
) {
reply.ok();
}
fn release(
&mut self,
_req: &Request<'_>,
_ino: u64,
_fh: u64,
_flags: i32,
_lock_owner: Option<u64>,
_flush: bool,
reply: ReplyEmpty,
) {
reply.ok();
}
fn fsync(
&mut self,
_req: &Request<'_>,
_ino: u64,
_fh: u64,
_datasync: bool,
reply: ReplyEmpty,
) {
reply.ok();
}
fn opendir(&mut self, _req: &Request<'_>, ino: u64, _flags: i32, reply: ReplyOpen) {
match self.inodes.get(ino).map(|record| &record.node.kind) {
Some(kind) if is_directory_like(kind) => reply.opened(0, 0),
Some(_) => reply.error(ENOTDIR),
None => reply.error(ENOENT),
}
}
fn readdir(
&mut self,
_req: &Request<'_>,
ino: u64,
_fh: u64,
offset: i64,
mut reply: ReplyDirectory,
) {
let start = match usize::try_from(offset) {
Ok(start) => start,
Err(_) => {
reply.error(EINVAL);
return;
}
};
match self.readdir_entries(ino) {
Ok(entries) => {
for entry in entries.into_iter().skip(start) {
if reply.add(entry.ino, entry.offset, entry.kind, entry.name) {
break;
}
}
reply.ok();
}
Err(errno) => reply.error(errno),
}
}
fn releasedir(
&mut self,
_req: &Request<'_>,
_ino: u64,
_fh: u64,
_flags: i32,
reply: ReplyEmpty,
) {
reply.ok();
}
fn fsyncdir(
&mut self,
_req: &Request<'_>,
_ino: u64,
_fh: u64,
_datasync: bool,
reply: ReplyEmpty,
) {
reply.ok();
}
fn statfs(&mut self, _req: &Request<'_>, _ino: u64, reply: ReplyStatfs) {
let files = u64::try_from(self.inodes.len()).unwrap_or(u64::MAX).max(1);
reply.statfs(1, 0, 0, files, 0, BLOCK_SIZE, 255, BLOCK_SIZE);
}
fn setxattr(
&mut self,
_req: &Request<'_>,
_ino: u64,
_name: &OsStr,
_value: &[u8],
_flags: i32,
_position: u32,
reply: ReplyEmpty,
) {
reply.error(EROFS);
}
fn getxattr(
&mut self,
_req: &Request<'_>,
ino: u64,
name: &OsStr,
size: u32,
reply: ReplyXattr,
) {
match self
.inodes
.get(ino)
.ok_or(ENOENT)
.and_then(|record| self.xattr_value(&record.node, name.as_bytes()))
{
Ok(value) => self.reply_xattr_bytes(reply, &value, size),
Err(errno) => reply.error(errno),
}
}
fn listxattr(&mut self, _req: &Request<'_>, ino: u64, size: u32, reply: ReplyXattr) {
match self.inodes.get(ino).ok_or(ENOENT) {
Ok(record) => match self.list_xattr_names(&record.node) {
Ok(names) => self.reply_xattr_bytes(reply, &names, size),
Err(errno) => reply.error(errno),
},
Err(errno) => reply.error(errno),
}
}
fn removexattr(&mut self, _req: &Request<'_>, _ino: u64, _name: &OsStr, reply: ReplyEmpty) {
reply.error(EROFS);
}
fn access(&mut self, _req: &Request<'_>, ino: u64, mask: i32, reply: ReplyEmpty) {
if self.inodes.get(ino).is_none() {
reply.error(ENOENT);
return;
}
if mask & W_OK != 0 {
reply.error(EACCES);
return;
}
reply.ok();
}
fn create(
&mut self,
_req: &Request<'_>,
_parent: u64,
_name: &OsStr,
_mode: u32,
_umask: u32,
_flags: i32,
reply: ReplyCreate,
) {
reply.error(EROFS);
}
fn getlk(
&mut self,
_req: &Request<'_>,
_ino: u64,
_fh: u64,
_lock_owner: u64,
_start: u64,
_end: u64,
_typ: i32,
_pid: u32,
reply: ReplyLock,
) {
reply.error(ENOSYS);
}
fn setlk(
&mut self,
_req: &Request<'_>,
_ino: u64,
_fh: u64,
_lock_owner: u64,
_start: u64,
_end: u64,
_typ: i32,
_pid: u32,
_sleep: bool,
reply: ReplyEmpty,
) {
reply.error(ENOSYS);
}
fn bmap(
&mut self,
_req: &Request<'_>,
_ino: u64,
_blocksize: u32,
_idx: u64,
reply: ReplyBmap,
) {
reply.error(ENOSYS);
}
fn ioctl(
&mut self,
_req: &Request<'_>,
_ino: u64,
_fh: u64,
_flags: u32,
_cmd: u32,
_in_data: &[u8],
_out_size: u32,
reply: ReplyIoctl,
) {
reply.error(ENOSYS);
}
fn fallocate(
&mut self,
_req: &Request<'_>,
_ino: u64,
_fh: u64,
_offset: i64,
_length: i64,
_mode: i32,
reply: ReplyEmpty,
) {
reply.error(EROFS);
}
fn lseek(
&mut self,
_req: &Request<'_>,
_ino: u64,
_fh: u64,
_offset: i64,
_whence: i32,
reply: ReplyLseek,
) {
reply.error(ENOSYS);
}
fn copy_file_range(
&mut self,
_req: &Request<'_>,
_ino_in: u64,
_fh_in: u64,
_offset_in: i64,
_ino_out: u64,
_fh_out: u64,
_offset_out: i64,
_len: u64,
_flags: u32,
reply: ReplyWrite,
) {
reply.error(EROFS);
}
}
fn is_directory_like(kind: &NodeKind) -> bool {
matches!(
kind,
NodeKind::Root
| NodeKind::AtResolver
| NodeKind::AtPrefix
| NodeKind::CommitsResolver
| NodeKind::HistoryResolver
| NodeKind::HistoryPrefix
| NodeKind::HistoryDirectory { .. }
| NodeKind::RefsRoot
| NodeKind::RefIndex { .. }
| NodeKind::Directory { .. }
| NodeKind::GitlinkDirectory { .. }
)
}
fn classify_revision_error(
filesystem: &HeadFilesystem,
_spec: &[u8],
error: StoreError,
) -> Result<RevisionLookup, i32> {
match error {
StoreError::RevisionNotFound { source, .. }
| StoreError::AmbiguousRevision { source, .. }
| StoreError::ResolveRevision { source, .. } => {
Ok(RevisionLookup::Missing(source.to_string()))
}
other => Err(filesystem.store_error_errno(other)),
}
}
fn node_from_entry(
source: Arc<GixRepository>,
entry: &TreeEntry,
context: RevisionContext,
) -> Node {
match entry.kind {
GitObjectKind::Tree => Node {
kind: NodeKind::Directory {
source,
tree: TreeInfo {
oid: entry.oid.clone(),
},
context,
},
},
GitObjectKind::Blob if entry.mode == 0o120000 => Node {
kind: NodeKind::Symlink {
source,
oid: entry.oid.clone(),
context,
},
},
GitObjectKind::Blob => Node {
kind: NodeKind::File {
source,
oid: entry.oid.clone(),
mode: entry.mode,
context,
},
},
GitObjectKind::Commit => Node {
kind: NodeKind::GitlinkDirectory {
oid: entry.oid.clone(),
context,
},
},
}
}
fn node_file_type(node: &Node) -> FileType {
match node.kind {
NodeKind::Root
| NodeKind::AtResolver
| NodeKind::AtPrefix
| NodeKind::CommitsResolver
| NodeKind::HistoryResolver
| NodeKind::HistoryPrefix
| NodeKind::HistoryDirectory { .. }
| NodeKind::RefsRoot
| NodeKind::RefIndex { .. }
| NodeKind::Directory { .. }
| NodeKind::GitlinkDirectory { .. } => FileType::Directory,
NodeKind::File { .. } | NodeKind::GitlinkFile { .. } => FileType::RegularFile,
NodeKind::Symlink { .. } => FileType::Symlink,
}
}
fn node_permissions(node: &Node) -> u16 {
match &node.kind {
NodeKind::Root
| NodeKind::AtResolver
| NodeKind::AtPrefix
| NodeKind::CommitsResolver
| NodeKind::HistoryResolver
| NodeKind::HistoryPrefix
| NodeKind::HistoryDirectory { .. }
| NodeKind::RefsRoot
| NodeKind::RefIndex { .. }
| NodeKind::Directory { .. }
| NodeKind::GitlinkDirectory { .. } => 0o555,
NodeKind::File { mode, .. } => {
if *mode == 0o100755 {
0o555
} else {
0o444
}
}
NodeKind::Symlink { .. } => 0o777,
NodeKind::GitlinkFile { .. } => 0o444,
}
}
fn format_git_mode(mode: u32) -> Vec<u8> {
format!("{mode:06o}").into_bytes()
}
fn history_versions(entries: &[HistoryEntry]) -> Arc<[HistoryVersion]> {
let width = entries.len().to_string().len().max(4);
let versions = entries
.iter()
.enumerate()
.map(|(index, entry)| HistoryVersion {
name: history_version_name(
index.saturating_add(1),
width,
&entry.commit.oid,
&entry.path,
),
entry: entry.clone(),
})
.collect::<Vec<_>>();
Arc::from(versions)
}
fn history_version_name(
ordinal: usize,
width: usize,
commit: &GitObjectId,
path: &[u8],
) -> Vec<u8> {
let mut name = format!("{ordinal:0width$}-").into_bytes();
let short_len = commit.as_hex().len().min(7);
name.extend_from_slice(&commit.as_hex().as_bytes()[..short_len]);
name.extend_from_slice(history_version_suffix(path));
name
}
fn history_version_suffix(path: &[u8]) -> &[u8] {
let basename = path.rsplit(|byte| *byte == b'/').next().unwrap_or(path);
if basename.first().is_some_and(|byte| *byte == b'.') {
return basename;
}
match basename.iter().position(|byte| *byte == b'.') {
Some(index) => basename.get(index..).unwrap_or_default(),
None => &[],
}
}
fn validate_tree_entry_name(name: &[u8]) -> Result<(), i32> {
if !is_valid_tree_entry_name(name) {
return Err(EIO);
}
Ok(())
}
fn child_key(parent: &NodeKey, name: &[u8]) -> NodeKey {
let mut components = match parent {
NodeKey::Root => Vec::new(),
NodeKey::Path(components) => components.clone(),
};
components.push(name.to_vec());
NodeKey::Path(components)
}
fn parent_key(key: &NodeKey) -> NodeKey {
match key {
NodeKey::Root => NodeKey::Root,
NodeKey::Path(components) => {
let mut parent = components.clone();
parent.pop();
if parent.is_empty() {
NodeKey::Root
} else {
NodeKey::Path(parent)
}
}
}
}
fn ttl_for_revision_spec(spec: &[u8]) -> TtlClass {
if is_hex_revision_spec(spec) {
TtlClass::Long
} else {
TtlClass::Short
}
}
fn gitlink_contents(oid: &GitObjectId) -> Vec<u8> {
let mut contents = oid.as_hex().as_bytes().to_vec();
contents.push(b'\n');
contents
}
fn read_limited(reader: &mut dyn Read, limit: usize) -> Result<Vec<u8>, i32> {
let mut limited = reader.take(u64::try_from(limit).map_err(|_| EIO)?);
let mut data = Vec::new();
limited.read_to_end(&mut data).map_err(|_| EIO)?;
Ok(data)
}
fn display_bytes(bytes: &[u8]) -> String {
String::from_utf8_lossy(bytes).into_owned()
}
fn commit_time_to_system_time(seconds: i64) -> SystemTime {
if seconds >= 0 {
match u64::try_from(seconds) {
Ok(seconds) => UNIX_EPOCH + Duration::from_secs(seconds),
Err(_) => UNIX_EPOCH,
}
} else {
UNIX_EPOCH - Duration::from_secs(seconds.unsigned_abs())
}
}
fn current_uid() -> u32 {
unsafe { libc::geteuid() }
}
fn current_gid() -> u32 {
unsafe { libc::getegid() }
}
fn size_to_blocks(size: u64) -> u64 {
if size == 0 {
0
} else {
1 + (size - 1) / 512
}
}