use crate::json_record::quote;
use crate::private_storage::sync_directory;
use crate::worktree_copy_error::{WorktreeCopyError, failure_with_cause, raw_failure};
use crate::worktree_copy_install_entry::InstalledPath;
use crate::worktree_copy_journal::{
Role, assert_private_path, encode_installed_entry, installed_entry, string_array,
};
use crate::worktree_copy_selection::assert_safe_repository_path;
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
use std::path::{Path, PathBuf};
pub const INSTALL_LOG_NAME: &str = "install-log.jsonl";
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct InstallLogRecords {
pub intended: Vec<String>,
pub created: Vec<InstalledPath>,
}
#[derive(Debug)]
pub struct InstallLog {
file: std::fs::File,
path: PathBuf,
}
#[derive(Debug)]
pub struct OpenedLog {
pub log: InstallLog,
pub recorded: InstallLogRecords,
}
fn parse_line(line: &str, path: &Path) -> Result<InstallLogRecords, WorktreeCopyError> {
let corrupt = || {
return failure_with_cause(
format!(
"cli-git: worktree-copy install log is corrupt: {}.",
quote(path.to_string_lossy().as_ref())
)
.as_str(),
None,
);
};
let value: serde_json::Value = serde_json::from_str(line).map_err(|_| return corrupt())?;
let object = value.as_object().ok_or_else(corrupt)?;
if let Some(intended) = object.get("intended").and_then(serde_json::Value::as_array) {
let names: Vec<String> = intended
.iter()
.map(|item| return item.as_str().map(str::to_string))
.collect::<Option<Vec<String>>>()
.ok_or_else(corrupt)?;
for name in &names {
assert_safe_repository_path(name.as_str())?;
}
return Ok(InstallLogRecords {
intended: names,
created: Vec::new(),
});
}
if let Some(created) = object.get("created").and_then(serde_json::Value::as_array) {
let entries: Vec<InstalledPath> = created
.iter()
.map(installed_entry)
.collect::<Option<Vec<InstalledPath>>>()
.ok_or_else(corrupt)?;
for entry in &entries {
assert_safe_repository_path(entry.relative_path.as_str())?;
}
return Ok(InstallLogRecords {
intended: Vec::new(),
created: entries,
});
}
return Err(corrupt());
}
fn read_complete_records(path: &Path) -> Result<(InstallLogRecords, u64, u64), WorktreeCopyError> {
assert_private_path(path, Role::JournalFile)?;
let content: Vec<u8> = std::fs::read(path).map_err(|error| return raw_failure(&error))?;
let complete_length: usize = content
.iter()
.rposition(|byte| return *byte == b'\n')
.map_or(0, |index| return index + 1);
let text: String = String::from_utf8_lossy(&content[..complete_length]).into_owned();
let mut records: InstallLogRecords = InstallLogRecords::default();
for line in text.split('\n').filter(|line| return !line.is_empty()) {
let parsed: InstallLogRecords = parse_line(line, path)?;
records.intended.extend(parsed.intended);
records.created.extend(parsed.created);
}
return Ok((records, complete_length as u64, content.len() as u64));
}
pub fn open_install_log(stage_container: &Path) -> Result<OpenedLog, WorktreeCopyError> {
let path: PathBuf = stage_container.join(INSTALL_LOG_NAME);
let existed: bool = std::fs::symlink_metadata(path.as_path()).is_ok();
let (recorded, complete_length, total_length) = if existed {
read_complete_records(path.as_path())?
} else {
(InstallLogRecords::default(), 0, 0)
};
let opened: std::io::Result<std::fs::File> = std::fs::OpenOptions::new()
.create(true)
.append(true)
.custom_flags(libc::O_NOFOLLOW)
.mode(0o600)
.open(path.as_path());
let file: std::fs::File = match opened {
Ok(file) => file,
Err(error) => {
let message: String = format!(
"cli-git: could not open worktree-copy install log {}.",
quote(path.to_string_lossy().as_ref())
);
return Err(failure_with_cause(message.as_str(), Some(&error)));
}
};
if complete_length < total_length {
file.set_len(complete_length)
.map_err(|error| return raw_failure(&error))?;
file.sync_all()
.map_err(|error| return raw_failure(&error))?;
}
if !existed {
sync_directory(stage_container).map_err(|error| return raw_failure(&error))?;
}
return Ok(OpenedLog {
log: InstallLog { file, path },
recorded,
});
}
impl InstallLog {
fn append_line(&mut self, line: &str) -> Result<(), WorktreeCopyError> {
let written: std::io::Result<()> = self
.file
.write_all(line.as_bytes())
.and_then(|()| return self.file.sync_all());
return written.map_err(|error| {
let message: String = format!(
"cli-git: could not persist worktree-copy install log {}.",
quote(self.path.to_string_lossy().as_ref())
);
return failure_with_cause(message.as_str(), Some(&error));
});
}
pub fn append_intents(&mut self, relative_paths: &[String]) -> Result<(), WorktreeCopyError> {
if relative_paths.is_empty() {
return Ok(());
}
let line: String = format!("{{\"intended\":{}}}\n", string_array(relative_paths));
return self.append_line(line.as_str());
}
pub fn append_creations(&mut self, entries: &[InstalledPath]) -> Result<(), WorktreeCopyError> {
if entries.is_empty() {
return Ok(());
}
let encoded: Vec<String> = entries.iter().map(encode_installed_entry).collect();
let line: String = format!("{{\"created\":[{}]}}\n", encoded.join(","));
return self.append_line(line.as_str());
}
}
#[cfg(test)]
#[path = "worktree_copy_install_log_tests.rs"]
mod tests;