use std::ops::BitAnd;
use hex::ToHex;
use rusqlite::{Row, Rows};
use crate::common::CvmfsResult;
#[derive(Debug, Copy, Clone)]
pub enum ContentHashTypes {
Unknown = -1,
Sha1 = 1,
Ripemd160 = 2,
Sha256 = 3,
Shake128 = 4,
}
impl ContentHashTypes {
pub fn hash_suffix(obj: &Self) -> String {
match obj {
ContentHashTypes::Ripemd160 => "-rmd160".into(),
ContentHashTypes::Sha256 => "-sha256".into(),
ContentHashTypes::Shake128 => "-shake128".into(),
_ => "".into(),
}
}
}
impl From<u32> for ContentHashTypes {
fn from(value: u32) -> Self {
match value {
1 => ContentHashTypes::Sha1,
2 => ContentHashTypes::Ripemd160,
3 => ContentHashTypes::Sha256,
4 => ContentHashTypes::Shake128,
_ => ContentHashTypes::Unknown,
}
}
}
#[derive(Debug, Copy, Clone)]
pub enum Flags {
Directory = 1,
NestedCatalogMountpoint = 2,
File = 4,
Link = 8,
FileStat = 16,
NestedCatalogRoot = 32,
FileChunk = 64,
ExternalFile = 2048,
ContentHashTypes = 256 + 512 + 1024,
}
impl BitAnd<Flags> for Flags {
type Output = u32;
fn bitand(self, rhs: Flags) -> Self::Output {
self as u32 & rhs
}
}
impl BitAnd<u32> for Flags {
type Output = u32;
fn bitand(self, rhs: u32) -> Self::Output {
self as u32 & rhs
}
}
impl BitAnd<Flags> for u32 {
type Output = u32;
fn bitand(self, rhs: Flags) -> Self::Output {
self & rhs as u32
}
}
#[derive(Debug, Clone)]
pub struct Chunk {
pub offset: i64,
pub size: i64,
pub content_hash: String,
pub content_hash_type: ContentHashTypes,
}
impl Chunk {
pub fn content_hash_string(&self) -> String {
format!("{}{}", &self.content_hash, ContentHashTypes::hash_suffix(&self.content_hash_type))
}
}
#[derive(Debug)]
pub struct PathHash {
pub hash1: i64,
pub hash2: i64,
}
#[derive(Debug)]
pub struct DirectoryEntryWrapper {
pub directory_entry: DirectoryEntry,
pub path: String,
}
#[derive(Debug, Clone)]
pub struct DirectoryEntry {
pub md5_path_1: i64,
pub md5_path_2: i64,
pub parent_1: i64,
pub parent_2: i64,
pub content_hash: Option<String>,
pub flags: u32,
pub size: i64,
pub mode: u16,
pub mtime: i64,
pub name: String,
pub symlink: Option<String>,
pub uid: u32,
pub gid: u32,
pub hardlinks: i64,
pub xattr: Option<Vec<u8>>,
pub content_hash_type: ContentHashTypes,
pub chunks: Vec<Chunk>,
}
impl DirectoryEntry {
pub fn new(row: &Row) -> CvmfsResult<Self> {
let content_hash: Option<Vec<u8>> = row.get(4)?;
let flags = row.get(5)?;
Ok(Self {
md5_path_1: row.get(0)?,
md5_path_2: row.get(1)?,
parent_1: row.get(2)?,
parent_2: row.get(3)?,
content_hash: content_hash.map(|value| value.encode_hex()),
flags,
size: row.get(6)?,
mode: row.get(7)?,
mtime: row.get(8)?,
name: row.get(9)?,
symlink: row.get(10)?,
uid: row.get(11)?,
gid: row.get(12)?,
hardlinks: row.get(13)?,
xattr: row.get(14)?,
content_hash_type: Self::read_content_hash_type(flags),
chunks: vec![],
})
}
pub fn add_chunks(&mut self, mut rows: Rows) -> CvmfsResult<()> {
self.chunks.clear();
loop {
match rows.next() {
Ok(row) => {
if let Some(row) = row {
let content_hash: Vec<u8> = row.get(4)?;
self.chunks.push(Chunk {
offset: row.get(2)?,
size: row.get(3)?,
content_hash: content_hash.encode_hex(),
content_hash_type: self.content_hash_type,
})
} else {
break;
}
}
Err(e) => return Err(e.into()),
}
}
Ok(())
}
pub fn is_directory(&self) -> bool {
self.flags & Flags::Directory > 0
}
pub fn is_nested_catalog_mountpoint(&self) -> bool {
self.flags & Flags::NestedCatalogMountpoint > 0
}
pub fn is_nested_catalog_root(&self) -> bool {
self.flags & Flags::NestedCatalogRoot > 0
}
pub fn is_file(&self) -> bool {
self.flags & Flags::File > 0
}
pub fn is_symlink(&self) -> bool {
self.flags & Flags::Link > 0
}
pub fn is_external_file(&self) -> bool {
self.flags & Flags::ExternalFile > 0
}
pub fn path_hash(&self) -> PathHash {
PathHash { hash1: self.md5_path_1, hash2: self.md5_path_2 }
}
pub fn parent_hash(&self) -> PathHash {
PathHash { hash1: self.parent_1, hash2: self.parent_2 }
}
pub fn nlink(&self) -> u32 {
(self.hardlinks & 0xFFFF_FFFF) as u32
}
pub fn hardlink_group(&self) -> u32 {
((self.hardlinks >> 32) & 0xFFFF_FFFF) as u32
}
pub fn has_chunks(&self) -> bool {
self.content_hash.is_none()
}
pub fn content_hash_string(&self) -> Option<String> {
self.content_hash.clone().map(|value| {
format!("{}{}", &value, ContentHashTypes::hash_suffix(&self.content_hash_type))
})
}
pub(crate) fn read_content_hash_type(flags: u32) -> ContentHashTypes {
let mut bit_mask = Flags::ContentHashTypes as u32;
let mut right_shifts = 0;
while (bit_mask & 1) == 0 {
bit_mask >>= 1;
right_shifts += 1;
}
(((flags & Flags::ContentHashTypes) >> right_shifts) + 1).into()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_entry(flags: u32, content_hash: Option<String>) -> DirectoryEntry {
DirectoryEntry {
md5_path_1: 1,
md5_path_2: 2,
parent_1: 3,
parent_2: 4,
content_hash,
flags,
size: 100,
mode: 0o644,
mtime: 1000,
name: "test".into(),
symlink: None,
uid: 1000,
gid: 1000,
hardlinks: 1,
xattr: None,
content_hash_type: DirectoryEntry::read_content_hash_type(flags),
chunks: vec![],
}
}
#[test]
fn hash_suffix_sha1_is_empty() {
assert_eq!(ContentHashTypes::hash_suffix(&ContentHashTypes::Sha1), "");
}
#[test]
fn hash_suffix_ripemd160() {
assert_eq!(ContentHashTypes::hash_suffix(&ContentHashTypes::Ripemd160), "-rmd160");
}
#[test]
fn hash_suffix_unknown_is_empty() {
assert_eq!(ContentHashTypes::hash_suffix(&ContentHashTypes::Unknown), "");
}
#[test]
fn hash_suffix_sha256() {
assert_eq!(ContentHashTypes::hash_suffix(&ContentHashTypes::Sha256), "-sha256");
}
#[test]
fn hash_suffix_shake128() {
assert_eq!(ContentHashTypes::hash_suffix(&ContentHashTypes::Shake128), "-shake128");
}
#[test]
fn content_hash_types_from_1_is_sha1() {
let t: ContentHashTypes = 1u32.into();
assert!(matches!(t, ContentHashTypes::Sha1));
}
#[test]
fn content_hash_types_from_2_is_ripemd160() {
let t: ContentHashTypes = 2u32.into();
assert!(matches!(t, ContentHashTypes::Ripemd160));
}
#[test]
fn content_hash_types_from_3_is_sha256() {
let t: ContentHashTypes = 3u32.into();
assert!(matches!(t, ContentHashTypes::Sha256));
}
#[test]
fn content_hash_types_from_4_is_shake128() {
let t: ContentHashTypes = 4u32.into();
assert!(matches!(t, ContentHashTypes::Shake128));
}
#[test]
fn content_hash_types_from_0_is_unknown() {
let t: ContentHashTypes = 0u32.into();
assert!(matches!(t, ContentHashTypes::Unknown));
}
#[test]
fn content_hash_types_from_99_is_unknown() {
let t: ContentHashTypes = 99u32.into();
assert!(matches!(t, ContentHashTypes::Unknown));
}
#[test]
fn u32_bitand_flags() {
let result: u32 = 7u32 & Flags::File;
assert_eq!(result, 4); }
#[test]
fn flags_bitand_u32() {
let result: u32 = Flags::File & 7u32;
assert_eq!(result, 4);
}
#[test]
fn flags_bitand_flags() {
let result: u32 = Flags::Directory & Flags::Directory;
assert_eq!(result, 1);
}
#[test]
fn flags_bitand_no_overlap() {
let result: u32 = Flags::Directory & Flags::File;
assert_eq!(result, 0);
}
#[test]
fn chunk_content_hash_string_sha1() {
let chunk = Chunk {
offset: 0,
size: 100,
content_hash: "abc123".into(),
content_hash_type: ContentHashTypes::Sha1,
};
assert_eq!(chunk.content_hash_string(), "abc123");
}
#[test]
fn chunk_content_hash_string_ripemd160() {
let chunk = Chunk {
offset: 0,
size: 100,
content_hash: "deadbeef".into(),
content_hash_type: ContentHashTypes::Ripemd160,
};
assert_eq!(chunk.content_hash_string(), "deadbeef-rmd160");
}
#[test]
fn entry_is_directory() {
let entry = make_entry(Flags::Directory as u32, Some("hash".into()));
assert!(entry.is_directory());
assert!(!entry.is_file());
assert!(!entry.is_symlink());
}
#[test]
fn entry_is_file() {
let entry = make_entry(Flags::File as u32, Some("hash".into()));
assert!(entry.is_file());
assert!(!entry.is_directory());
assert!(!entry.is_symlink());
}
#[test]
fn entry_is_symlink() {
let entry = make_entry(Flags::Link as u32, Some("hash".into()));
assert!(entry.is_symlink());
assert!(!entry.is_file());
assert!(!entry.is_directory());
}
#[test]
fn entry_is_nested_catalog_mountpoint() {
let entry = make_entry(Flags::NestedCatalogMountpoint as u32, Some("hash".into()));
assert!(entry.is_nested_catalog_mountpoint());
}
#[test]
fn entry_is_nested_catalog_root() {
let entry = make_entry(Flags::NestedCatalogRoot as u32, Some("hash".into()));
assert!(entry.is_nested_catalog_root());
}
#[test]
fn entry_has_chunks_when_no_content_hash() {
let entry = make_entry(Flags::File as u32, None);
assert!(entry.has_chunks());
}
#[test]
fn entry_no_chunks_when_content_hash_present() {
let entry = make_entry(Flags::File as u32, Some("abc".into()));
assert!(!entry.has_chunks());
}
#[test]
fn entry_content_hash_string_with_sha1() {
let entry = make_entry(Flags::File as u32, Some("abc123".into()));
assert_eq!(entry.content_hash_string(), Some("abc123".to_string()));
}
#[test]
fn entry_content_hash_string_with_ripemd160() {
let flags = Flags::File as u32 | 256;
let entry = make_entry(flags, Some("abc123".into()));
assert_eq!(entry.content_hash_string(), Some("abc123-rmd160".to_string()));
}
#[test]
fn entry_content_hash_string_none_for_chunked() {
let entry = make_entry(Flags::File as u32, None);
assert_eq!(entry.content_hash_string(), None);
}
#[test]
fn entry_path_hash() {
let entry = make_entry(Flags::File as u32, Some("h".into()));
let ph = entry.path_hash();
assert_eq!(ph.hash1, 1);
assert_eq!(ph.hash2, 2);
}
#[test]
fn entry_parent_hash() {
let entry = make_entry(Flags::File as u32, Some("h".into()));
let ph = entry.parent_hash();
assert_eq!(ph.hash1, 3);
assert_eq!(ph.hash2, 4);
}
#[test]
fn read_content_hash_type_sha1_from_zero_hash_bits() {
let t = DirectoryEntry::read_content_hash_type(0);
assert!(matches!(t, ContentHashTypes::Sha1));
}
#[test]
fn read_content_hash_type_ripemd160_from_256() {
let t = DirectoryEntry::read_content_hash_type(256);
assert!(matches!(t, ContentHashTypes::Ripemd160));
}
#[test]
fn read_content_hash_type_sha256_from_512() {
let t = DirectoryEntry::read_content_hash_type(512);
assert!(matches!(t, ContentHashTypes::Sha256));
}
#[test]
fn read_content_hash_type_shake128_from_768() {
let t = DirectoryEntry::read_content_hash_type(768);
assert!(matches!(t, ContentHashTypes::Shake128));
}
#[test]
fn read_content_hash_type_ignores_non_hash_flags() {
let t = DirectoryEntry::read_content_hash_type(Flags::File as u32);
assert!(matches!(t, ContentHashTypes::Sha1));
}
#[test]
fn entry_combined_flags() {
let flags = Flags::File as u32 | Flags::FileChunk as u32;
let entry = make_entry(flags, None);
assert!(entry.is_file());
assert!(!entry.is_directory());
assert!(entry.has_chunks());
}
#[test]
fn entry_nlink() {
let mut entry = make_entry(Flags::File as u32, Some("h".into()));
entry.hardlinks = 3;
assert_eq!(entry.nlink(), 3);
}
#[test]
fn entry_hardlink_group() {
let mut entry = make_entry(Flags::File as u32, Some("h".into()));
entry.hardlinks = (5_i64 << 32) | 2;
assert_eq!(entry.nlink(), 2);
assert_eq!(entry.hardlink_group(), 5);
}
#[test]
fn entry_uid_gid() {
let entry = make_entry(Flags::File as u32, Some("h".into()));
assert_eq!(entry.uid, 1000);
assert_eq!(entry.gid, 1000);
}
#[test]
fn entry_xattr_none() {
let entry = make_entry(Flags::File as u32, Some("h".into()));
assert!(entry.xattr.is_none());
}
#[test]
fn entry_is_external_file() {
let flags = Flags::File as u32 | Flags::ExternalFile as u32;
let entry = make_entry(flags, Some("h".into()));
assert!(entry.is_external_file());
assert!(entry.is_file());
}
#[test]
fn entry_is_not_external_file() {
let entry = make_entry(Flags::File as u32, Some("h".into()));
assert!(!entry.is_external_file());
}
}