blob: b3d262548f787cb66c1138344c743f7629831275 [file] [log] [blame] [edit]
// Copyright 2024 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
mod ffi;
use std::fs::File;
use std::io::{Error, ErrorKind, Read, Result, Seek, SeekFrom};
use std::os::unix::fs::MetadataExt;
use std::path::Path;
use crate::ffi::ffi::SeekLocation;
/// Represents a (virtual) log file.
///
/// Applications shouldn't make assumptions about files stored in this structure
/// or where it's reading data from. The storage location is an implementation
/// detail and users of the API shouldn't try to read data themselves.
///
/// # Example
/// ```rust
/// let mut log_file = LogFile::new("test_log_file").expect("test log file exists");
/// let mut data = vec![0; log_file.get_file_length() as usize];
/// log_file.read(data.as_mut_slice());
/// println!("{data:?}");
/// ```
#[derive(Debug)]
pub struct LogFile {
file: File,
}
/// Make sure the usize fits into an i64.
///
/// Will return i64::MAX if the value would be bigger.
///
/// ```rust
/// assert_eq!(capping_usize_to_i64(usize::MAX), i64::MAX);
/// assert_eq!(capping_usize_to_i64(1), 1);
/// ```
fn capping_usize_to_i64(val: usize) -> i64 {
if usize::BITS <= u64::BITS {
capping_u64_to_i64(val as u64)
} else {
i64::MAX
}
}
/// Make sure the u64 fits into an i64.
///
/// Will return i64::MAX if the value would be bigger.
///
/// ```rust
/// assert_eq!(capping_u64_to_i64(u64::MAX), i64::MAX);
/// assert_eq!(capping_u64_to_i64(1), 1);
/// ```
fn capping_u64_to_i64(val: u64) -> i64 {
0_i64.checked_add_unsigned(val).unwrap_or(i64::MAX)
}
/// If a positive value is passed it will be converted to it's negative value.
///
/// ```rust
/// assert_eq!(ensure_i32_is_negative(-1), -1);
/// assert_eq!(ensure_i32_is_negative(1), -1);
/// ```
fn ensure_i32_is_negative(val: i32) -> i32 {
if val > 0 {
return -val;
} else {
return val;
}
}
impl LogFile {
/// Create a new virtual log file.
///
/// ```rust
/// assert!(LogFile::new("test_log_file").is_ok());
/// assert!(LogFile::new("a/path").is_err());
/// ```
pub fn new(name: &str) -> Result<Self> {
const PATH_SEPARATORS: &[char; 2] = &['/', '\\'];
if name.contains(PATH_SEPARATORS) {
return Err(Error::new(
ErrorKind::InvalidInput,
"File name must not contain path separator",
));
}
log::trace!("Create new logfile from name '{name}'");
let file_path = Self::file_path(name);
let file = File::open(&file_path)?;
Ok(LogFile { file })
}
/// Create a new logfile for the specified `path`.
///
/// ```rust
/// assert!(LogFile::new("test_log_file").is_ok());
/// assert!(LogFile::new("./tests/test_log_file").is_ok());
/// assert!(LogFile::new("/var/log/messages").is_ok());
/// assert!(LogFile::new("a/path/that/doesnt/exist").is_err());
/// ```
pub fn from_path(path: &str) -> Result<Self> {
log::trace!("Create new logfile from path '{path}'");
let file = File::open(&path)?;
Ok(LogFile { file })
}
fn file_path(name: &str) -> String {
let path = format!("{}/{}", Self::log_path_prefix(), name);
log::trace!("Generated filepath for '{name}' is '{path}'");
path
}
pub fn path_exists(path: &str) -> bool {
Path::new(path).exists()
}
pub fn exists(name: &str) -> bool {
Self::path_exists(Self::file_path(name).as_str())
}
fn log_path_prefix() -> &'static str {
#[cfg(test)]
return concat!(env!("CARGO_MANIFEST_DIR"), "/tests/");
#[cfg(not(test))]
"/var/log/"
}
/// Read data from the current position in the file.
///
/// At most `min(i64::MAX, usize::MAX)` data is going to be valid in `buf`.
pub fn read(&mut self, buf: &mut [u8]) -> std::io::Result<i64> {
const MAX_READ: usize = i64::MAX as usize;
let read = if buf.len() > MAX_READ {
log::debug!(
"Requested read of size {}, capping at {MAX_READ}",
buf.len()
);
self.file.read(&mut buf[..MAX_READ])?
} else {
self.file.read(buf)?
};
Ok(capping_usize_to_i64(read))
}
/// Get the inode value of the current file.
///
/// Returns `0` in case of a problem.
pub fn get_inode(&self) -> u64 {
match self.file.metadata() {
Ok(metadata) => {
return metadata.ino();
}
Err(err) => {
log::error!("Couldn't get inode because: {err}");
// Technically not an invalid value but unusual enough to indicate a
// problem.
return 0;
}
}
}
/// Get the length of the file in bytes.
///
/// At most `i64::MAX` bytes will be returned even if the file is larger.
/// In case of an error a negative value is returned.
pub fn get_file_length(&self) -> i64 {
match self.file.metadata() {
Ok(metadata) => {
let size = metadata.len();
return capping_u64_to_i64(size);
}
Err(err) => {
if let Some(os_err) = err.raw_os_error() {
return ensure_i32_is_negative(os_err) as i64;
}
}
}
// Generic negative value.
return -1;
}
/// Set the current file position to `location`.
pub fn seek(&mut self, location: SeekLocation) -> bool {
let position = match location {
SeekLocation::Begin => SeekFrom::Start(0),
SeekLocation::BeforeEnd => SeekFrom::End(-1),
SeekLocation::End => SeekFrom::End(0),
SeekLocation {
repr: 3_u8..=u8::MAX,
} => {
log::error!("Received invalid enum value '{location:?}' as seek location.");
return false;
}
};
match self.file.seek(position) {
Ok(_) => true,
Err(err) => {
log::error!("Couldn't seek to {location:?} because: {err}");
false
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
pub struct TestLogFile {
log_file: NamedTempFile,
}
impl TestLogFile {
pub fn new() -> Self {
TestLogFile {
log_file: NamedTempFile::new().expect("able to create temp file"),
}
}
pub fn with_content(data: &str) -> Self {
let mut log_file = NamedTempFile::new().expect("able to create temp file");
write!(log_file, "{}", data).expect("can write to temp file");
TestLogFile { log_file }
}
pub fn get_path(&self) -> &str {
self.log_file.path().to_str().unwrap()
}
pub fn as_logfile(&self) -> LogFile {
LogFile::from_path(self.get_path()).unwrap()
}
}
fn buf_to_str(buf: &Vec<u8>, size: i64) -> &str {
std::str::from_utf8(&buf[..size as usize]).unwrap()
}
// stat -c %s soul/gravedigger/tests/test_log_file
const TEST_FILE_LENGTH: i64 = 56;
#[test]
fn no_path_allowed() {
assert!(LogFile::new("/Some/Absolute/Path").is_err());
assert!(LogFile::new("Some/Relative/Path").is_err());
assert!(LogFile::new("C:\\\\WinPath").is_err());
assert!(LogFile::new("test_log_file").is_ok());
}
#[test]
fn file_length() {
let lf = LogFile::new("test_log_file").unwrap();
assert_eq!(lf.get_file_length(), TEST_FILE_LENGTH);
let empty_file = TestLogFile::new();
assert_eq!(empty_file.as_logfile().get_file_length(), 0);
}
#[test]
fn read_test_file() {
let data = "Hello World";
let logfile = TestLogFile::with_content(data);
let mut buf = vec![0; 1024];
let read_bytes = logfile.as_logfile().read(buf.as_mut_slice()).unwrap();
assert_eq!(read_bytes, data.len() as i64);
assert_eq!(data, buf_to_str(&buf, read_bytes));
}
#[test]
fn read_test_file_intervals() {
let data = "Hello World";
let test_file = TestLogFile::with_content(data);
let mut logfile = test_file.as_logfile();
let interval = data.len() / 3;
let mut buf = vec![0; interval];
let interval = interval as i64;
for _ in 0..interval {
let read_bytes = logfile.read(buf.as_mut_slice()).unwrap();
assert_eq!(read_bytes, interval);
}
let read_bytes = logfile.read(buf.as_mut_slice()).unwrap();
assert_eq!(read_bytes, (data.len() % 3) as i64);
}
#[test]
fn read_test_file_large_buf() {
let mut lf = LogFile::new("test_log_file").unwrap();
let mut buf = vec![0; 1000];
assert!((lf.get_file_length() as usize) < buf.len());
let read_bytes = lf.read(buf.as_mut_slice()).unwrap();
assert_eq!(read_bytes, TEST_FILE_LENGTH);
}
#[test]
fn seek_begin() {
let data = "Hello World";
let test_file = TestLogFile::with_content(data);
let mut logfile = test_file.as_logfile();
let mut buf = vec![0; data.len()];
assert_eq!(logfile.read(buf.as_mut_slice()).unwrap(), data.len() as i64);
assert_eq!(logfile.read(buf.as_mut_slice()).unwrap(), 0);
assert!(logfile.seek(SeekLocation::Begin));
assert_eq!(logfile.read(buf.as_mut_slice()).unwrap(), data.len() as i64);
}
#[test]
fn seek_before_end() {
let data = "Hello World";
let test_file = TestLogFile::with_content(data);
let mut logfile = test_file.as_logfile();
let mut buf = vec![0; data.len()];
assert_eq!(logfile.get_file_length(), data.len() as i64);
assert!(logfile.seek(SeekLocation::BeforeEnd));
assert_eq!(logfile.read(buf.as_mut_slice()).unwrap(), 1);
}
#[test]
fn seek_end() {
let data = "Hello World";
let test_file = TestLogFile::with_content(data);
let mut logfile = test_file.as_logfile();
let mut buf = vec![0; data.len()];
assert_eq!(logfile.get_file_length(), data.len() as i64);
assert!(logfile.seek(SeekLocation::End));
assert_eq!(logfile.read(buf.as_mut_slice()).unwrap(), 0);
}
#[test]
fn seek_invalid() {
let mut logfile = LogFile::new("test_log_file").unwrap();
assert!(!logfile.seek(SeekLocation { repr: 4_u8 }));
}
}