use std::{
borrow::Cow,
ffi::{OsStr, OsString},
fmt::{self, Debug},
ops::Deref,
os::unix::ffi::OsStringExt,
path::{self, Path, PathBuf},
};
use rusqlite::{ToSql, types::FromSql};
use crate::{FileId, RootId};
#[derive(Clone, PartialEq, Eq)]
pub enum FileOrigin<'a, T = FileBy<'a>> {
Host {
path: Cow<'a, Path>,
},
Litebox {
locator: T,
root: RootId,
},
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl fmt::Debug for FileOrigin<'_, FileBy<'_>> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FileOrigin::Host { path } => write!(f, "file://{}", path.to_string_lossy()),
FileOrigin::Litebox { locator, root } => match locator {
FileBy::Id(id) => write!(f, "litebox:{root}:{id:?}"),
FileBy::Path(path) => {
write!(f, "litebox:{root}:{}", path.to_string_lossy())
}
},
}
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl fmt::Debug for FileOrigin<'_, &Path> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FileOrigin::Host { path } => write!(f, "file://{}", path.to_string_lossy()),
FileOrigin::Litebox { locator, root } => {
write!(f, "litebox:{root}:{}", locator.to_string_lossy())
}
}
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl fmt::Debug for FileOrigin<'_, PathBuf> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FileOrigin::Host { path } => write!(f, "file://{}", path.to_string_lossy()),
FileOrigin::Litebox { locator, root } => {
write!(
f,
"{:?}",
FileOrigin::Litebox {
root: *root,
locator: locator.as_ref()
}
)
}
}
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl fmt::Debug for FileOrigin<'_, Cow<'_, Path>> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FileOrigin::Host { path } => write!(f, "file://{}", path.to_string_lossy()),
FileOrigin::Litebox { locator, root } => {
write!(
f,
"{:?}",
FileOrigin::Litebox {
root: *root,
locator: locator.as_ref()
}
)
}
}
}
}
#[derive(Clone, PartialEq, Eq)]
pub enum FileBy<'a> {
Id(FileId),
Path(Cow<'a, Path>),
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl Debug for FileBy<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FileBy::Id(id) => f.debug_tuple("FileId").field(id).finish(),
FileBy::Path(path) => path.fmt(f),
}
}
}
impl<'a> FileBy<'a> {
pub(crate) fn into_normalized(self) -> FileBy<'static> {
match self {
FileBy::Id(id) => FileBy::Id(id),
FileBy::Path(path) => FileBy::Path(Cow::Owned(NormalizedPath::new(path).to_path_buf())),
}
}
}
impl From<FileId> for FileBy<'static> {
fn from(value: FileId) -> Self {
FileBy::Id(value)
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl<'a> From<&'a FileId> for FileBy<'static> {
fn from(value: &'a FileId) -> Self {
FileBy::Id(*value)
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl From<PathBuf> for FileBy<'static> {
fn from(value: PathBuf) -> Self {
FileBy::Path(Cow::Owned(value))
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl<'a> From<&'a PathBuf> for FileBy<'a> {
fn from(value: &'a PathBuf) -> Self {
FileBy::Path(Cow::Borrowed(value.as_ref()))
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl<'a> From<&'a Path> for FileBy<'a> {
fn from(value: &'a Path) -> Self {
FileBy::Path(Cow::Borrowed(value))
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl From<OsString> for FileBy<'static> {
fn from(value: OsString) -> Self {
FileBy::Path(Cow::Owned(PathBuf::from(value)))
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl<'a> From<&'a OsStr> for FileBy<'a> {
fn from(value: &'a std::ffi::OsStr) -> Self {
FileBy::Path(Cow::Borrowed(Path::new(value)))
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl From<String> for FileBy<'static> {
fn from(value: String) -> Self {
FileBy::Path(Cow::Owned(PathBuf::from(value)))
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl<'a> From<&'a String> for FileBy<'a> {
fn from(value: &'a String) -> Self {
FileBy::Path(Cow::Borrowed(Path::new(value)))
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl<'a> From<&'a str> for FileBy<'a> {
fn from(value: &'a str) -> Self {
FileBy::Path(Cow::Borrowed(Path::new(value)))
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl From<NormalizedPath> for FileBy<'static> {
fn from(value: NormalizedPath) -> Self {
FileBy::Path(Cow::Owned(value.into()))
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl<'a> From<&'a NormalizedPath> for FileBy<'a> {
fn from(value: &'a NormalizedPath) -> Self {
FileBy::Path(Cow::Borrowed(value.as_ref()))
}
}
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct NormalizedPath {
inner: PathBuf,
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl Debug for NormalizedPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.inner.fmt(f)
}
}
impl NormalizedPath {
pub fn new<P: AsRef<Path>>(path: P) -> Self {
let mut new_path = PathBuf::new();
new_path.push("/");
for component in path.as_ref().components() {
match component {
path::Component::Normal(segment) => {
new_path.push(segment);
}
path::Component::RootDir => {}
path::Component::Prefix(_) => {
}
path::Component::CurDir => {}
path::Component::ParentDir => {
new_path.pop();
}
}
}
Self { inner: new_path }
}
pub fn as_bytes(&self) -> &[u8] {
self.inner.as_os_str().as_encoded_bytes()
}
pub fn components(&self) -> impl Iterator<Item = &OsStr> {
self.inner
.components()
.filter_map(|component| match component {
path::Component::Prefix(_) => unreachable!(),
path::Component::RootDir => None,
path::Component::CurDir => unreachable!(),
path::Component::ParentDir => unreachable!(),
path::Component::Normal(segment) => Some(segment),
})
}
}
impl Deref for NormalizedPath {
type Target = Path;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl AsRef<Path> for NormalizedPath {
fn as_ref(&self) -> &Path {
&self.inner
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl AsRef<[u8]> for NormalizedPath {
fn as_ref(&self) -> &[u8] {
self.as_bytes()
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl From<&Path> for NormalizedPath {
fn from(value: &Path) -> Self {
Self::new(value)
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl From<PathBuf> for NormalizedPath {
fn from(value: PathBuf) -> Self {
Self::new(value)
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl From<Vec<u8>> for NormalizedPath {
fn from(value: Vec<u8>) -> Self {
Self::new(OsString::from_vec(value))
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl From<NormalizedPath> for PathBuf {
fn from(value: NormalizedPath) -> Self {
value.inner
}
}
#[doc(hidden)]
impl ToSql for NormalizedPath {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
self.as_bytes().to_sql()
}
}
#[doc(hidden)]
impl FromSql for NormalizedPath {
fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
let bytes: Vec<u8> = rusqlite::types::FromSql::column_result(value)?;
let os_str = OsString::from_vec(bytes);
Ok(NormalizedPath::new(os_str))
}
}
#[cfg(test)]
mod tests {
use super::*;
use xpct::{equal, expect};
#[test]
fn relative_path_becomes_absolute() {
let path = NormalizedPath::new("foo/bar");
expect!(path.to_path_buf()).to(equal(PathBuf::from("/foo/bar")));
}
#[test]
fn absolute_path_stays_absolute() {
let path = NormalizedPath::new("/foo/bar");
expect!(path.to_path_buf()).to(equal(PathBuf::from("/foo/bar")));
}
#[test]
fn current_dir_at_start_removed() {
let path = NormalizedPath::new("./foo");
expect!(path.to_path_buf()).to(equal(PathBuf::from("/foo")));
}
#[test]
fn current_dir_in_middle_removed() {
let path = NormalizedPath::new("foo/./bar");
expect!(path.to_path_buf()).to(equal(PathBuf::from("/foo/bar")));
}
#[test]
fn parent_dir_at_end_navigates_up() {
let path = NormalizedPath::new("/foo/bar/..");
expect!(path.to_path_buf()).to(equal(PathBuf::from("/foo")));
}
#[test]
fn parent_dir_in_middle_navigates_up() {
let path = NormalizedPath::new("/foo/bar/../baz");
expect!(path.to_path_buf()).to(equal(PathBuf::from("/foo/baz")));
}
#[test]
fn parent_at_root_stays_at_root() {
let path = NormalizedPath::new("/../foo");
expect!(path.to_path_buf()).to(equal(PathBuf::from("/foo")));
}
#[test]
fn parent_at_root_yields_root() {
let path = NormalizedPath::new("/..");
expect!(path.to_path_buf()).to(equal(PathBuf::from("/")));
}
#[test]
fn empty_path_becomes_root() {
let path = NormalizedPath::new("");
expect!(path.to_path_buf()).to(equal(PathBuf::from("/")));
}
#[test]
fn root_stays_root() {
let path = NormalizedPath::new("/");
expect!(path.to_path_buf()).to(equal(PathBuf::from("/")));
}
#[test]
fn trailing_slash_removed() {
let path = NormalizedPath::new("/foo/bar/");
expect!(path.to_path_buf()).to(equal(PathBuf::from("/foo/bar")));
}
#[test]
fn multiple_slashes_collapsed() {
let path = NormalizedPath::new("foo//bar");
expect!(path.to_path_buf()).to(equal(PathBuf::from("/foo/bar")));
}
#[test]
fn multiple_parent_dirs() {
let path = NormalizedPath::new("/a/b/c/../../d");
expect!(path.to_path_buf()).to(equal(PathBuf::from("/a/d")));
}
}