use crate::fs_beneath::ObjectId;
use crate::subvolume_error::MigrateError;
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::AsRawFd;
use std::path::{Path, PathBuf};
pub const JOURNAL_NAME: &str = "cli-git-convert";
pub const LOCK_NAME: &str = "cli-git-convert.lock";
pub const CLAIM_PREFIX: &str = "cli-git-convert.owner";
const SMALL_FILE_LIMIT: u64 = 1 << 20;
const JOURNAL_HEADER: &str = "cli-git-convert v1";
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum Phase {
Started,
Creating,
Synced,
Exchanged,
Refreshed,
Removed,
}
impl Phase {
fn name(self) -> &'static str {
return match self {
Phase::Started => "started",
Phase::Creating => "creating",
Phase::Synced => "synced",
Phase::Exchanged => "exchanged",
Phase::Refreshed => "refreshed",
Phase::Removed => "removed",
};
}
fn parse(text: &str) -> Option<Phase> {
for phase in [Phase::Started, Phase::Creating, Phase::Synced, Phase::Exchanged, Phase::Refreshed, Phase::Removed] {
if phase.name() == text {
return Some(phase);
}
}
return None;
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Journal {
pub phase: Phase,
pub token: String,
pub path: Vec<u8>,
pub old_identity: ObjectId,
pub git_file: Vec<u8>,
pub new_identity: Option<ObjectId>,
}
fn unknown_shape() -> MigrateError {
return MigrateError::Journal(String::from("the conversion journal has a shape this command does not know; nothing was changed"));
}
fn field_of(text: &str, name: &str) -> Option<String> {
for line in text.lines() {
if let Some(rest) = line.strip_prefix(name)
&& let Some(value) = rest.strip_prefix('=')
{
return Some(String::from(value));
}
}
return None;
}
fn number_from_digits<Number: std::str::FromStr>(digits: String) -> Option<Number> {
return digits.parse().ok();
}
fn sync_directory_of(admin: &Path) -> std::io::Result<()> {
return match std::fs::File::open(admin) {
Ok(directory) => directory.sync_all(),
Err(error) => Err(error),
};
}
pub fn io_step<T>(outcome: std::io::Result<T>, action: &str, path: &Path) -> Result<T, MigrateError> {
return match outcome {
Ok(value) => Ok(value),
Err(error) => Err(MigrateError::io(action, path, &error)),
};
}
fn hex(bytes: &[u8]) -> String {
let mut spelled: String = String::new();
for byte in bytes {
spelled.push_str(format!("{byte:02x}").as_str());
}
return spelled;
}
fn unhex(text: &str) -> Option<Vec<u8>> {
if text.len() % 2 != 0 {
return None;
}
let mut bytes: Vec<u8> = Vec::new();
let mut at: usize = 0;
while at < text.len() {
let pair: &str = text.get(at..at + 2)?;
bytes.push(u8::from_str_radix(pair, 16).ok()?);
at += 2;
}
return Some(bytes);
}
fn temporary_name() -> String {
let nanos: u128 = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
Ok(elapsed) => elapsed.as_nanos(),
Err(_before_epoch) => 0,
};
return format!("{JOURNAL_NAME}.tmp.{}.{nanos}", std::process::id());
}
impl Journal {
fn encode(&self) -> String {
return format!(
"{JOURNAL_HEADER}\nphase={}\ntoken={}\npath={}\nold_identity={}\ngit_file={}\nnew_identity={}\n",
self.phase.name(),
self.token,
hex(self.path.as_slice()),
self.old_identity.encode(),
hex(self.git_file.as_slice()),
match self.new_identity {
Some(identity) => identity.encode(),
None => String::from("-"),
}
);
}
fn decode(text: &str) -> Result<Journal, MigrateError> {
let mut lines = text.lines();
if lines.next() != Some(JOURNAL_HEADER) {
return Err(unknown_shape());
}
let new_identity: Option<ObjectId> = match field_of(text, "new_identity").as_deref() {
None | Some("-") => None,
Some(encoded) => Some(ObjectId::decode(encoded).ok_or_else(unknown_shape)?),
};
let old_identity: ObjectId = match field_of(text, "old_identity") {
Some(encoded) => ObjectId::decode(encoded.as_str()).ok_or_else(unknown_shape)?,
None => ObjectId {
device: field_of(text, "old_device").and_then(number_from_digits).ok_or_else(unknown_shape)?,
inode: field_of(text, "old_inode").and_then(number_from_digits).ok_or_else(unknown_shape)?,
birth: None,
},
};
return Ok(Journal {
phase: Phase::parse(field_of(text, "phase").ok_or_else(unknown_shape)?.as_str()).ok_or_else(unknown_shape)?,
token: field_of(text, "token").ok_or_else(unknown_shape)?,
path: unhex(field_of(text, "path").ok_or_else(unknown_shape)?.as_str()).ok_or_else(unknown_shape)?,
old_identity,
git_file: unhex(field_of(text, "git_file").ok_or_else(unknown_shape)?.as_str()).ok_or_else(unknown_shape)?,
new_identity,
});
}
fn write_temporary(&self, admin: &Path) -> std::io::Result<PathBuf> {
let temporary: PathBuf = admin.join(temporary_name());
let mut file: std::fs::File = std::fs::OpenOptions::new().write(true).create_new(true).mode(0o600).open(&temporary)?;
let written: std::io::Result<()> = match file.write_all(self.encode().as_bytes()) {
Ok(()) => file.sync_all(),
Err(error) => Err(error),
};
if let Err(error) = written {
let _removed: std::io::Result<()> = std::fs::remove_file(&temporary);
return Err(error);
}
return Ok(temporary);
}
pub fn save(&self, admin: &Path) -> Result<(), MigrateError> {
let target: PathBuf = admin.join(JOURNAL_NAME);
return io_step(self.write_and_publish(admin, target.as_path()), "write the conversion journal", target.as_path());
}
fn write_and_publish(&self, admin: &Path, target: &Path) -> std::io::Result<()> {
let temporary: PathBuf = self.write_temporary(admin)?;
if let Err(error) = std::fs::rename(&temporary, target) {
let _removed: std::io::Result<()> = std::fs::remove_file(&temporary);
return Err(error);
}
return sync_directory_of(admin);
}
pub fn create_new(&self, admin: &Path) -> Result<(), MigrateError> {
let target: PathBuf = admin.join(JOURNAL_NAME);
return io_step(self.write_and_link(admin, target.as_path()), "create the conversion journal", target.as_path());
}
fn write_and_link(&self, admin: &Path, target: &Path) -> std::io::Result<()> {
let temporary: PathBuf = self.write_temporary(admin)?;
let linked: std::io::Result<()> = std::fs::hard_link(&temporary, target);
let _removed: std::io::Result<()> = std::fs::remove_file(&temporary);
linked?;
return sync_directory_of(admin);
}
pub fn load(admin: &Path) -> Result<Option<Journal>, MigrateError> {
let path: PathBuf = admin.join(JOURNAL_NAME);
match read_plain_file(path.as_path(), SMALL_FILE_LIMIT) {
Ok(bytes) => return Journal::decode(String::from_utf8_lossy(bytes.as_slice()).as_ref()).map(Some),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(MigrateError::io("read the conversion journal", path.as_path(), &error)),
}
}
}
pub fn read_plain_file(path: &Path, limit: u64) -> std::io::Result<Vec<u8>> {
let file: std::fs::File = std::fs::OpenOptions::new().read(true).custom_flags(libc::O_NONBLOCK | libc::O_NOFOLLOW | libc::O_CLOEXEC).open(path)?;
if !file.metadata()?.file_type().is_file() {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "it is not a plain file (a named pipe, a device or a directory)"));
}
let mut bytes: Vec<u8> = Vec::new();
std::io::Read::read_to_end(&mut std::io::Read::take(&file, limit + 1), &mut bytes)?;
if bytes.len() as u64 > limit {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "it is larger than this command reads"));
}
return Ok(bytes);
}
pub fn read_plain_text(path: &Path) -> std::io::Result<String> {
return match String::from_utf8(read_plain_file(path, SMALL_FILE_LIMIT)?) {
Ok(text) => Ok(text),
Err(_not_text) => Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "it is not text")),
};
}
pub fn is_recorded_object(recorded: &ObjectId, found: &ObjectId) -> bool {
return recorded.device == found.device && recorded.inode == found.inode && (recorded.birth.is_none() || recorded.birth == found.birth);
}
pub fn legacy_lock_is_held(admin: &Path) -> Result<bool, MigrateError> {
let path: PathBuf = admin.join(LOCK_NAME);
let file: std::fs::File = match std::fs::OpenOptions::new().read(true).write(true).custom_flags(libc::O_NONBLOCK | libc::O_NOFOLLOW | libc::O_CLOEXEC).open(&path) {
Ok(opened) => opened,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false),
Err(error) => return Err(MigrateError::io("open the conversion lock an older release left", path.as_path(), &error)),
};
let plain: bool = io_step(file.metadata(), "examine the conversion lock an older release left", path.as_path())?.file_type().is_file();
if !plain {
return Ok(false);
}
let result: libc::c_int = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
if result != 0 {
let error: std::io::Error = std::io::Error::last_os_error();
if error.kind() == std::io::ErrorKind::WouldBlock {
return Ok(true);
}
return Err(MigrateError::io("probe the conversion lock an older release left", path.as_path(), &error));
}
return match std::fs::remove_file(&path) {
Ok(()) => Ok(false),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(error) => Err(MigrateError::io("remove the conversion lock an older release left", path.as_path(), &error)),
};
}