use log::{debug, error};
use serde::{Deserialize, Serialize};
use std::ffi::OsString;
use std::fs::read_link;
use std::fs::{FileType, Metadata};
use std::os::unix::ffi::OsStringExt;
use std::os::unix::fs::FileTypeExt;
use std::path::{Path, PathBuf};
use users::{Groups, Users, UsersCache};
#[cfg(target_os = "linux")]
use std::os::linux::fs::MetadataExt;
#[cfg(target_os = "macos")]
use std::os::macos::fs::MetadataExt;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FilesystemEntry {
kind: FilesystemKind,
path: Vec<u8>,
len: u64,
mode: u32,
mtime: i64,
mtime_ns: i64,
atime: i64,
atime_ns: i64,
symlink_target: Option<PathBuf>,
uid: u32,
gid: u32,
user: String,
group: String,
}
#[derive(Debug, thiserror::Error)]
pub enum FsEntryError {
#[error("Unknown file kind {0}")]
UnknownFileKindCode(u8),
#[error("failed to read symbolic link target {0}: {1}")]
ReadLink(PathBuf, std::io::Error),
}
#[allow(clippy::len_without_is_empty)]
impl FilesystemEntry {
pub fn from_metadata(
path: &Path,
meta: &Metadata,
cache: &mut UsersCache,
) -> Result<Self, FsEntryError> {
let kind = FilesystemKind::from_file_type(meta.file_type());
Ok(EntryBuilder::new(kind)
.path(path.to_path_buf())
.len(meta.len())
.mode(meta.st_mode())
.mtime(meta.st_mtime(), meta.st_mtime_nsec())
.atime(meta.st_atime(), meta.st_atime_nsec())
.user(meta.st_uid(), cache)?
.group(meta.st_uid(), cache)?
.symlink_target()?
.build())
}
pub fn kind(&self) -> FilesystemKind {
self.kind
}
pub fn pathbuf(&self) -> PathBuf {
let path = self.path.clone();
PathBuf::from(OsString::from_vec(path))
}
pub fn len(&self) -> u64 {
self.len
}
pub fn mode(&self) -> u32 {
self.mode
}
pub fn atime(&self) -> i64 {
self.atime
}
pub fn atime_ns(&self) -> i64 {
self.atime_ns
}
pub fn mtime(&self) -> i64 {
self.mtime
}
pub fn mtime_ns(&self) -> i64 {
self.mtime_ns
}
pub fn is_dir(&self) -> bool {
self.kind() == FilesystemKind::Directory
}
pub fn symlink_target(&self) -> Option<PathBuf> {
self.symlink_target.clone()
}
}
#[derive(Debug)]
pub(crate) struct EntryBuilder {
kind: FilesystemKind,
path: PathBuf,
len: u64,
mode: u32,
mtime: i64,
mtime_ns: i64,
atime: i64,
atime_ns: i64,
symlink_target: Option<PathBuf>,
uid: u32,
gid: u32,
user: String,
group: String,
}
impl EntryBuilder {
pub(crate) fn new(kind: FilesystemKind) -> Self {
Self {
kind,
path: PathBuf::new(),
len: 0,
mode: 0,
mtime: 0,
mtime_ns: 0,
atime: 0,
atime_ns: 0,
symlink_target: None,
uid: 0,
user: "".to_string(),
gid: 0,
group: "".to_string(),
}
}
pub(crate) fn build(self) -> FilesystemEntry {
FilesystemEntry {
kind: self.kind,
path: self.path.into_os_string().into_vec(),
len: self.len,
mode: self.mode,
mtime: self.mtime,
mtime_ns: self.mtime_ns,
atime: self.atime,
atime_ns: self.atime_ns,
symlink_target: self.symlink_target,
uid: self.uid,
user: self.user,
gid: self.gid,
group: self.group,
}
}
pub(crate) fn path(mut self, path: PathBuf) -> Self {
self.path = path;
self
}
pub(crate) fn len(mut self, len: u64) -> Self {
self.len = len;
self
}
pub(crate) fn mode(mut self, mode: u32) -> Self {
self.mode = mode;
self
}
pub(crate) fn mtime(mut self, secs: i64, nsec: i64) -> Self {
self.mtime = secs;
self.mtime_ns = nsec;
self
}
pub(crate) fn atime(mut self, secs: i64, nsec: i64) -> Self {
self.atime = secs;
self.atime_ns = nsec;
self
}
pub(crate) fn symlink_target(mut self) -> Result<Self, FsEntryError> {
self.symlink_target = if self.kind == FilesystemKind::Symlink {
debug!("reading symlink target for {:?}", self.path);
let target = read_link(&self.path)
.map_err(|err| FsEntryError::ReadLink(self.path.clone(), err))?;
Some(target)
} else {
None
};
Ok(self)
}
pub(crate) fn user(mut self, uid: u32, cache: &mut UsersCache) -> Result<Self, FsEntryError> {
self.uid = uid;
self.user = if let Some(user) = cache.get_user_by_uid(uid) {
user.name().to_string_lossy().to_string()
} else {
"".to_string()
};
Ok(self)
}
pub(crate) fn group(mut self, gid: u32, cache: &mut UsersCache) -> Result<Self, FsEntryError> {
self.gid = gid;
self.group = if let Some(group) = cache.get_group_by_gid(gid) {
group.name().to_string_lossy().to_string()
} else {
"".to_string()
};
Ok(self)
}
}
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub enum FilesystemKind {
Regular,
Directory,
Symlink,
Socket,
Fifo,
}
impl FilesystemKind {
pub fn from_file_type(file_type: FileType) -> Self {
if file_type.is_file() {
FilesystemKind::Regular
} else if file_type.is_dir() {
FilesystemKind::Directory
} else if file_type.is_symlink() {
FilesystemKind::Symlink
} else if file_type.is_socket() {
FilesystemKind::Socket
} else if file_type.is_fifo() {
FilesystemKind::Fifo
} else {
panic!("unknown file type {:?}", file_type);
}
}
pub fn as_code(&self) -> u8 {
match self {
FilesystemKind::Regular => 0,
FilesystemKind::Directory => 1,
FilesystemKind::Symlink => 2,
FilesystemKind::Socket => 3,
FilesystemKind::Fifo => 4,
}
}
pub fn from_code(code: u8) -> Result<Self, FsEntryError> {
match code {
0 => Ok(FilesystemKind::Regular),
1 => Ok(FilesystemKind::Directory),
2 => Ok(FilesystemKind::Symlink),
3 => Ok(FilesystemKind::Socket),
4 => Ok(FilesystemKind::Fifo),
_ => Err(FsEntryError::UnknownFileKindCode(code)),
}
}
}
#[cfg(test)]
mod test {
use super::FilesystemKind;
#[test]
fn file_kind_regular_round_trips() {
one_file_kind_round_trip(FilesystemKind::Regular);
one_file_kind_round_trip(FilesystemKind::Directory);
one_file_kind_round_trip(FilesystemKind::Symlink);
one_file_kind_round_trip(FilesystemKind::Socket);
one_file_kind_round_trip(FilesystemKind::Fifo);
}
fn one_file_kind_round_trip(kind: FilesystemKind) {
assert_eq!(kind, FilesystemKind::from_code(kind.as_code()).unwrap());
}
}