use std::fs;
use std::io::{self, Seek};
use std::os::unix::fs::{DirBuilderExt, OpenOptionsExt};
use std::path::{Path, PathBuf};
use std::str;
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use rand::{rngs::OsRng, Rng};
use crate::support::error::Error;
use crate::support::file_ops::{self, IgnoreKinds};
#[derive(Debug, Clone, Copy)]
pub struct HierIdScheme<'a> {
pub prefix: u8,
pub extension: &'a str,
pub root: &'a Path,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AllocationPath(PathBuf);
impl AllocationPath {
fn alloc_path(&self) -> &Path {
self.0.parent().unwrap()
}
fn containing_directory(&self) -> &Path {
self.alloc_path().parent().unwrap()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccessPath(PathBuf);
impl AccessPath {
pub fn assume_exists(self) -> PathBuf {
self.0
}
}
impl<'a> HierIdScheme<'a> {
fn path_for_id(&self, id: u32, for_allocation: bool) -> PathBuf {
let first_octet: u32;
let mut first_name = [self.prefix, 0];
if id < 1 << 8 {
first_name[1] = b'0';
first_octet = 3;
} else if id < 1 << 16 {
first_name[1] = b'1';
first_octet = 2;
} else if id < 1 << 24 {
first_name[1] = b'2';
first_octet = 1;
} else {
first_name[1] = b'3';
first_octet = 0;
}
let mut buf = self.root.join(str::from_utf8(&first_name).unwrap());
for octet in first_octet..3 {
buf.push(format!("{:02x}", (id >> (8 * (3 - octet))) & 0xFF));
}
if for_allocation {
buf.push("alloc");
}
buf.push(format!("{:02x}.{}", id & 0xFF, self.extension));
buf
}
pub fn allocation_path_for_id(&self, id: u32) -> AllocationPath {
AllocationPath(self.path_for_id(id, true))
}
pub fn access_path_for_id(&self, id: u32) -> AccessPath {
AccessPath(self.path_for_id(id, false))
}
pub fn emplace(&self, src: &Path, dst_id: u32) -> Result<bool, Error> {
let dst = self.allocation_path_for_id(dst_id);
if 1 == dst_id || 0 == dst_id % 256 {
self.mkdirs(&dst)?;
}
match nix::unistd::linkat(
None,
src,
None,
&dst.0,
nix::unistd::LinkatFlags::SymlinkFollow,
) {
Ok(_) => Ok(true),
Err(nix::Error::Sys(nix::errno::Errno::EEXIST))
| Err(nix::Error::Sys(nix::errno::Errno::ELOOP)) => Ok(false),
Err(e) => Err(e.into()),
}
}
pub fn emplace_many(
&self,
srcs: &[&Path],
tmp: &Path,
max_id: u32,
) -> Result<u32, Error> {
const BASE: u32 = 256 * 256 * 256;
let (allocation_size, levels) = match srcs.len() {
0 => panic!("emplace_many with 0 items"),
1 => panic!("emplace_many with 1 item"),
2..=256 => (256, 1),
257..=65536 => (65536, 2),
_ => return Err(Error::BatchTooBig),
};
let tmp_root = tempfile::TempDir::new_in(tmp)?;
let isolated = HierIdScheme {
prefix: self.prefix,
extension: self.extension,
root: tmp_root.path(),
};
for (ix, src) in srcs.iter().enumerate() {
if !isolated.emplace(src, BASE + (ix as u32))? {
match fs::metadata(src) {
Ok(_) => return Err(Error::GaveUpInsertion),
Err(e) if io::ErrorKind::NotFound == e.kind() => {
return Err(Error::NxMessage);
}
Err(e) if Some(nix::libc::ELOOP) == e.raw_os_error() => {
return Err(Error::ExpungedMessage);
}
Err(e) => return Err(e.into()),
}
}
}
let first_id = self.first_unallocated_id();
if max_id.saturating_sub(first_id) < allocation_size {
return Err(Error::MailboxFull);
}
let target_id = (first_id + allocation_size - 1) / allocation_size
* allocation_size;
if max_id.saturating_sub(first_id) < srcs.len() as u32 {
return Err(Error::MailboxFull);
}
let mut to_allocate = first_id;
while to_allocate < target_id {
let id = to_allocate;
let gravestone = self.allocation_path_for_id(id);
if 0 == id % 256 {
let parent = gravestone.containing_directory();
self.mkdirs_bare(&parent)?;
let success = match std::os::unix::fs::symlink(
parent.file_name().unwrap(),
parent,
) {
Ok(_) => true,
Err(e) if Some(nix::libc::ELOOP) == e.raw_os_error() => {
true
}
_ => false,
};
if success {
to_allocate += 256;
continue;
}
self.mkdirs(&gravestone)?;
}
if 1 == id {
self.mkdirs(&gravestone)?;
}
self.expunge_path(id, gravestone.alloc_path(), tmp)?;
to_allocate = (id + 256) / 256 * 256;
}
let mut atomic_src = isolated.path_for_id(BASE, false);
let mut atomic_dst = self.path_for_id(target_id, false);
for _ in 0..levels {
atomic_src.pop();
atomic_dst.pop();
}
atomic_src.set_extension("d");
let atomic_dst_nominal = atomic_dst.clone();
atomic_dst.set_extension("d");
self.mkdirs_bare(&atomic_dst)?;
fs::rename(&atomic_src, &atomic_dst).map_err(|e| {
if Some(nix::libc::ENOTEMPTY) == e.raw_os_error()
|| Some(nix::libc::ELOOP) == e.raw_os_error()
|| io::ErrorKind::AlreadyExists == e.kind()
{
Error::GaveUpInsertion
} else {
e.into()
}
})?;
std::os::unix::fs::symlink(
atomic_dst.file_name().unwrap(),
atomic_dst_nominal,
)
.ignore_already_exists()?;
Ok(target_id)
}
pub fn expunge(&self, target: u32, tmp: &Path) -> Result<(), Error> {
let path = self.allocation_path_for_id(target);
self.expunge_path(target, &path.0, tmp)
}
fn expunge_path(
&self,
target: u32,
path: &Path,
tmp: &Path,
) -> Result<(), Error> {
let mut stage: PathBuf;
match fs::metadata(&path) {
Err(e) if Some(nix::libc::ELOOP) == e.raw_os_error() => {
return Ok(())
}
_ => (),
}
loop {
stage =
tmp.join(format!("expunge.{}.{}", target, OsRng.gen::<u64>()));
match std::os::unix::fs::symlink(path.file_name().unwrap(), &stage)
{
Ok(_) => break,
Err(e) if io::ErrorKind::AlreadyExists == e.kind() => continue,
Err(e) => return Err(e.into()),
}
}
let rename_res = fs::rename(&stage, &path);
if rename_res.is_err() {
let _ = fs::remove_file(&stage);
}
match rename_res {
Ok(_) => Ok(()),
Err(e) if Some(nix::libc::ELOOP) == e.raw_os_error() => Ok(()),
Err(e) => Err(e.into()),
}
}
pub fn is_allocated(&self, id: u32) -> bool {
match fs::metadata(&self.allocation_path_for_id(id).0) {
Ok(_) => true,
Err(e) if Some(nix::libc::ELOOP) == e.raw_os_error() => true,
_ => false,
}
}
pub fn first_unallocated_id(&self) -> u32 {
let mut guess_file = fs::OpenOptions::new()
.create(true)
.read(true)
.write(true)
.mode(0o660)
.open(self.root.join(format!("{}-guess", self.prefix as char)))
.ok();
let guess = guess_file
.as_mut()
.and_then(|f| f.read_u32::<LittleEndian>().ok())
.unwrap_or(1)
.max(1);
let result = probe_for_first_id(guess, |id| self.is_allocated(id));
if let Some(mut f) = guess_file {
let _ = f
.seek(io::SeekFrom::Start(0))
.and_then(|_| f.write_u32::<LittleEndian>(result));
}
result
}
fn mkdirs(&self, path: &AllocationPath) -> io::Result<()> {
self.mkdirs_bare(path.alloc_path())?;
match std::os::unix::fs::symlink(".", path.alloc_path()) {
Ok(_) => (),
Err(e) if io::ErrorKind::AlreadyExists == e.kind() => (),
Err(e) if Some(nix::libc::ELOOP) == e.raw_os_error() => (),
Err(e) => return Err(e),
}
Ok(())
}
fn mkdirs_bare(&self, path: &Path) -> io::Result<()> {
match fs::symlink_metadata(path.parent().unwrap()) {
Ok(_) => return Ok(()),
Err(e) if Some(nix::libc::ELOOP) == e.raw_os_error() => {
return Ok(())
}
_ => (),
}
let path = path.strip_prefix(&self.root).unwrap();
let mut iter = path.parent().unwrap().iter();
let mut cur_path =
self.root.join(iter.next().expect("No path components?"));
fs::DirBuilder::new()
.mode(0o770)
.create(&cur_path)
.ignore_already_exists()?;
for component in iter {
cur_path.push(component);
match fs::symlink_metadata(&cur_path) {
Ok(_) => continue,
Err(e) if Some(nix::libc::ELOOP) == e.raw_os_error() => {
return Ok(());
}
Err(e) if io::ErrorKind::NotFound == e.kind() => {
cur_path.set_extension("d");
match fs::DirBuilder::new()
.mode(0o770)
.create(&cur_path)
.ignore_already_exists()
{
Ok(_) => (),
Err(e)
if Some(nix::libc::ELOOP) == e.raw_os_error() =>
{
return Ok(());
}
Err(e) => return Err(e),
}
let target = cur_path.file_name().unwrap().to_owned();
cur_path.set_file_name(component);
match std::os::unix::fs::symlink(&target, &cur_path) {
Ok(()) => (),
Err(e) if
io::ErrorKind::AlreadyExists == e.kind() ||
Some(nix::libc::ELOOP) == e.raw_os_error() => (),
Err(e) => return Err(e),
}
}
Err(e) => return Err(e),
}
}
Ok(())
}
pub fn gc(
&self,
tmp: &Path,
garbage: &Path,
expunge_less_than: u32,
) -> Result<(), Error> {
let mut path = self.root.to_owned();
for i in 0..=3 {
path.push(str::from_utf8(&[self.prefix, b'0' + i]).unwrap());
if path.is_dir() {
if 0 == i {
self.gc_leaf(&mut path, tmp, 0, expunge_less_than, true)?;
} else {
self.gc_branch(
&mut path,
tmp,
garbage,
0,
8 * (i as u32),
expunge_less_than,
true,
)?;
}
}
path.pop();
}
Ok(())
}
fn gc_branch(
&self,
path: &mut PathBuf,
tmp: &Path,
garbage: &Path,
id_prefix: u32,
shift: u32,
expunge_less_than: u32,
top: bool,
) -> Result<bool, Error> {
if !top && (id_prefix | ((256 << shift) - 1)) < expunge_less_than {
return Ok(true);
}
let mut any_exist = false;
let mut all_allocated = true;
for i in 0..=255 {
let name = format!("{:02x}", i);
path.push(&name);
let (allocated, mut exists) = match fs::metadata(&path) {
Ok(_) => (true, true),
Err(e) if Some(nix::libc::ELOOP) == e.raw_os_error() => {
(true, false)
}
Err(e) if io::ErrorKind::NotFound == e.kind() => (false, false),
Err(e) => return Err(e.into()),
};
if exists {
path.set_extension("d");
let subprefix = id_prefix | (i << shift);
let can_remove = if 8 == shift {
self.gc_leaf(
path,
tmp,
subprefix,
expunge_less_than,
false,
)?
} else {
self.gc_branch(
path,
tmp,
garbage,
subprefix,
shift - 8,
expunge_less_than,
false,
)?
};
path.set_file_name(&name);
if can_remove {
self.expunge_path(subprefix, &path, tmp)?;
exists = false;
}
}
if allocated && !exists {
path.set_extension("d");
file_ops::delete_async(&path, garbage).ignore_not_found()?;
}
path.pop();
any_exist |= exists;
all_allocated &= allocated;
if !allocated && (i != 0 || !top) {
break;
}
}
Ok(!any_exist && all_allocated)
}
fn gc_leaf(
&self,
path: &mut PathBuf,
tmp: &Path,
id_prefix: u32,
expunge_less_than: u32,
top: bool,
) -> Result<bool, Error> {
if !top && (id_prefix | 0xFF) < expunge_less_than {
return Ok(true);
}
path.push("alloc");
let fully_allocated = match fs::metadata(&path) {
Err(e) => Some(nix::libc::ELOOP) == e.raw_os_error(),
Ok(_) => false,
};
path.pop();
let mut expunged_paths = Vec::new();
let mut all_gone = true;
for i in (0..=255).rev() {
path.push(format!("{:02x}.{}", i, self.extension));
let (allocated, mut exists) = match fs::metadata(&path) {
Ok(_) => (true, true),
Err(e) if Some(nix::libc::ELOOP) == e.raw_os_error() => {
(true, false)
}
Err(e) if io::ErrorKind::NotFound == e.kind() => (false, false),
Err(e) => return Err(e.into()),
};
if exists && (id_prefix | i) < expunge_less_than {
self.expunge_path(id_prefix | i, &path, tmp)?;
exists = false;
}
if allocated && !exists {
expunged_paths.push(path.clone());
}
path.pop();
if !allocated && !fully_allocated {
return Ok(false);
}
all_gone &= !exists;
}
if !all_gone && !expunged_paths.is_empty() {
if !fully_allocated {
path.push("alloc");
self.expunge_path(id_prefix, &path, tmp)?;
path.pop();
}
for path in expunged_paths {
let _ = fs::remove_file(path);
}
}
Ok(all_gone)
}
}
fn probe_for_first_id(guess: u32, exists: impl Fn(u32) -> bool) -> u32 {
let mut maximum_used = 0u32;
let mut minimum_free = u32::MAX;
let mut exp_probe = 1u32;
while exp_probe != 0 {
let probe = guess.saturating_add(exp_probe);
if u32::MAX == probe {
break;
}
exp_probe <<= 1;
if exists(probe) {
maximum_used = probe;
} else {
minimum_free = probe;
break;
}
}
if 0 == maximum_used && exists(guess) {
maximum_used = guess;
}
while maximum_used < minimum_free - 1 {
let mid = ((maximum_used as u64 + minimum_free as u64) / 2) as u32;
if exists(mid) {
maximum_used = mid;
} else {
minimum_free = mid;
}
}
maximum_used.saturating_add(1)
}
#[cfg(test)]
mod test {
use proptest::prelude::*;
use super::*;
fn test_probe_for_first_id(guess: u32, last_used: u32) -> u32 {
probe_for_first_id(guess, |u| u <= last_used)
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 65536,
..ProptestConfig::default()
})]
#[test]
fn id_probing_fuzz(guess in 1u32.., last_used in 0u32..) {
prop_assert_eq!(last_used.saturating_add(1),
test_probe_for_first_id(guess, last_used));
}
}
#[test]
fn id_probing_special_cases() {
assert_eq!(1, test_probe_for_first_id(1, 0));
assert_eq!(2, test_probe_for_first_id(1, 1));
assert_eq!(3, test_probe_for_first_id(1, 2));
assert_eq!(1001, test_probe_for_first_id(1000, 1000));
assert_eq!(1002, test_probe_for_first_id(1000, 1001));
assert_eq!(1011, test_probe_for_first_id(1000, 1010));
assert_eq!(u32::MAX, test_probe_for_first_id(1, u32::MAX));
}
#[test]
fn test_path_for_id() {
let scheme = HierIdScheme {
root: "".as_ref(),
prefix: b'z',
extension: "eml",
};
assert_eq!(
["z0", "01.eml"].iter().collect::<PathBuf>(),
scheme.access_path_for_id(1).0
);
assert_eq!(
["z0", "ff.eml"].iter().collect::<PathBuf>(),
scheme.access_path_for_id(255).0
);
assert_eq!(
["z1", "30", "39.eml"].iter().collect::<PathBuf>(),
scheme.access_path_for_id(12345).0
);
assert_eq!(
["z2", "01", "e2", "40.eml"].iter().collect::<PathBuf>(),
scheme.access_path_for_id(123456).0
);
assert_eq!(
["z3", "01", "00", "00", "00.eml"]
.iter()
.collect::<PathBuf>(),
scheme.access_path_for_id(16777216).0
);
assert_eq!(
["z3", "ff", "ff", "ff", "ff.eml"]
.iter()
.collect::<PathBuf>(),
scheme.access_path_for_id(4294967295).0
);
}
#[test]
fn test_hier_id_scheme() {
let root = tempfile::TempDir::new().unwrap();
let dummy = root.path().join("dummy");
let scheme = HierIdScheme {
prefix: b'x',
extension: "foo",
root: root.path(),
};
const N: u32 = 70_000;
for i in 1..N {
if 1 == i % 1024 {
let _ = fs::remove_file(&dummy);
file_ops::spit(
root.path(),
&dummy,
false,
0o600,
"dummy".as_bytes(),
)
.unwrap();
}
assert_eq!(i, scheme.first_unallocated_id());
assert!(scheme.emplace(&dummy, i).unwrap());
}
for i in 1..N {
assert!(scheme.is_allocated(i));
}
assert!(!scheme.is_allocated(N));
assert!(root.path().join("x1/02/00.foo").is_file());
scheme.expunge(512, root.path()).unwrap();
assert!(!root.path().join("x1/02/00.foo").is_file());
assert!(fs::symlink_metadata(root.path().join("x1/02/00.foo")).is_ok());
assert!(root.path().join("x1/0b/b8.foo").is_file());
scheme.expunge(3000, root.path()).unwrap();
assert!(!root.path().join("x1/0b/b8.foo").is_file());
assert!(fs::symlink_metadata(root.path().join("x1/0b/b8.foo")).is_ok());
scheme.expunge(N - 1, root.path()).unwrap();
for i in 1024..1024 + 256 {
scheme.expunge(i, root.path()).unwrap();
}
scheme.gc(root.path(), root.path(), 550).unwrap();
assert!(!root.path().join("x0/80.foo").is_file());
assert!(!root.path().join("x1/02/00.foo").is_file());
assert!(!root.path().join("x1/01").is_dir());
assert!(!root.path().join("x1/01.d").is_dir());
assert!(!root.path().join("x1/04").is_dir());
assert!(!root.path().join("x1/04.d").is_dir());
assert!(root.path().join("x1/02").is_dir());
assert!(root.path().join("x1/02.d").is_dir());
assert!(fs::symlink_metadata(root.path().join("x1/0b/b8.foo")).is_err());
assert!(scheme.is_allocated(1));
assert!(scheme.is_allocated(256));
assert!(scheme.is_allocated(512));
assert!(scheme.is_allocated(513));
assert!(scheme.is_allocated(1024));
assert!(scheme.is_allocated(3000));
assert!(scheme.is_allocated(N - 1));
assert!(!scheme.is_allocated(N));
assert!(!scheme.emplace(&dummy, N - 1).unwrap());
assert!(!scheme.emplace(&dummy, 549).unwrap());
assert!(!scheme.emplace(&dummy, 256).unwrap());
scheme.expunge(N - 1, root.path()).unwrap();
scheme.expunge(549, root.path()).unwrap();
scheme.expunge(256, root.path()).unwrap();
for entry in fs::read_dir(root.path()).unwrap() {
let entry = entry.unwrap();
assert!(!entry
.file_name()
.to_string_lossy()
.starts_with("expunge"));
}
}
#[test]
fn test_emplace_many() {
use rayon::prelude::*;
[
0u32, 1, 128, 254, 255, 256, 257, 510, 511, 512, 513, 65534, 65535,
65536, 131071, 131072,
]
.par_iter()
.for_each(|&num_prefix_messages| {
for &num_new_messages in &[2u32, 255, 256, 257, 1024, 65536] {
do_test_emplace_many(num_prefix_messages, num_new_messages);
}
});
}
fn do_test_emplace_many(num_prefix_messages: u32, num_new_messages: u32) {
let root = tempfile::TempDir::new().unwrap();
let scheme = HierIdScheme {
prefix: b'x',
extension: "foo",
root: root.path(),
};
for i in 1..=num_prefix_messages {
let dummy = root.path().join(format!("dummy{}", i));
fs::File::create(&dummy).unwrap();
assert!(scheme.emplace(&dummy, i).unwrap());
}
let mut path_bufs = Vec::new();
for i in 0..num_new_messages {
let dummy = root.path().join(format!("newdummy{}", i));
fs::File::create(&dummy).unwrap();
path_bufs.push(dummy);
}
let paths: Vec<&Path> =
path_bufs.iter().map(|pb| pb as &Path).collect();
let base = scheme.emplace_many(&paths, root.path(), 1 << 30).unwrap();
let fast_verify_end = base.saturating_sub(500).max(1);
for i_100 in 1..(fast_verify_end / 100).max(1) {
let i = i_100 * 100;
assert!(scheme.is_allocated(i), "ID {} not allocated", i);
}
for i in fast_verify_end..base + num_new_messages {
assert!(scheme.is_allocated(i), "ID {} not allocated", i);
}
}
#[test]
#[ignore]
fn test_hier_id_scheme_huge() {
let root = tempfile::TempDir::new().unwrap();
let dummy = root.path().join("dummy");
let scheme = HierIdScheme {
prefix: b'x',
extension: "foo",
root: root.path(),
};
const N: u32 = 20_000_000;
for i in 1..N {
if 1 == i % 1024 {
let _ = fs::remove_file(&dummy);
file_ops::spit(
root.path(),
&dummy,
false,
0o600,
"dummy".as_bytes(),
)
.unwrap();
}
assert_eq!(i, scheme.first_unallocated_id());
assert!(scheme.emplace(&dummy, i).unwrap());
}
for i in 1..N {
assert!(scheme.is_allocated(i));
}
for i in 1..N - 100 {
if !is_power_of_7(i) {
scheme.expunge(i, root.path()).unwrap();
}
}
scheme.gc(root.path(), root.path(), 0).unwrap();
for i in 1..N {
assert_eq!(
i >= N - 100 || is_power_of_7(i),
scheme.access_path_for_id(i).assume_exists().is_file(),
"Unexpected value for {}",
i
);
}
}
fn is_power_of_7(mut n: u32) -> bool {
while n > 1 && n % 7 == 0 {
n /= 7;
}
1 == n
}
}