#![forbid(clippy::arithmetic_side_effects)]
use std::{
fmt,
hash::{Hash, Hasher},
io,
io::{Cursor, Write},
os::fd::{AsFd, BorrowedFd, RawFd},
sync::{atomic::Ordering, OnceLock},
thread::ThreadId,
time::{SystemTime, UNIX_EPOCH},
};
use btoi::btoi;
use lexis::ToName;
use libseccomp::ScmpArch;
use nix::{
errno::Errno,
unistd::{getpid, gettid, Pid, Uid},
};
use crate::{
config::*,
err::SydResult,
fd::{is_active_fd, is_writable_fd, set_append},
hash::hex_encode_lower,
id::SydId,
ioctl::{Ioctl, IoctlMap, IoctlName},
proc::{proc_cmdline, proc_comm, proc_cwd, proc_tty},
sandbox::Action,
syslog::LogLevel,
};
#[macro_use]
mod macros;
mod map;
pub(crate) mod rlimit;
pub mod syslog;
pub use map::{LogMap, LogValue};
pub(crate) struct WriteBuf(pub(crate) Vec<u8>);
impl WriteBuf {
pub(crate) fn new() -> Self {
Self(Vec::new())
}
pub fn try_reserve(&mut self, additional: usize) -> Result<(), io::Error> {
self.0
.try_reserve(additional)
.or(Err(io::Error::from(io::ErrorKind::OutOfMemory)))
}
#[cfg(feature = "log")]
pub(crate) fn into_inner(self) -> Vec<u8> {
self.0
}
}
impl Default for WriteBuf {
fn default() -> Self {
Self::new()
}
}
impl io::Write for WriteBuf {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0
.try_reserve(buf.len())
.or(Err(io::Error::from(io::ErrorKind::OutOfMemory)))?;
self.0.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
static GLOBAL_LOG_RLIMIT: OnceLock<rlimit::LogRlimit> = OnceLock::new();
pub(crate) fn global_log_rlimit() -> &'static rlimit::LogRlimit {
GLOBAL_LOG_RLIMIT.get_or_init(|| {
rlimit::LogRlimit::new(DEFAULT_LOG_RLIMIT_INTERVAL_NS, DEFAULT_LOG_RLIMIT_BURST)
})
}
pub(crate) fn set_log_rlimit_interval(ns: u64) {
global_log_rlimit().set_interval_ns(ns);
}
pub(crate) fn set_log_rlimit_burst(burst: u32) {
global_log_rlimit().set_burst(burst);
}
use std::sync::Mutex;
pub(crate) static LOG_WRITE_LOCK: Mutex<()> = Mutex::new(());
pub(crate) fn write_all_to_fd(fd: BorrowedFd<'_>, mut buf: &[u8]) -> Result<(), nix::errno::Errno> {
use crate::cookie::safe_write;
while !buf.is_empty() {
match safe_write(fd, buf) {
Ok(0) => return Err(nix::errno::Errno::EIO),
Ok(n) => buf = &buf[n..],
Err(nix::errno::Errno::EINTR) => continue,
Err(e) => return Err(e),
}
}
Ok(())
}
pub(crate) fn emit_json_to_fd(
fd: BorrowedFd<'_>,
msg: &LogMap,
tty: bool,
level: crate::syslog::LogLevel,
) {
if cfg!(feature = "kcov") && level <= crate::syslog::LogLevel::Warn {
const MSG_KEYS: &[&str] = &["err", "error", "file", "line", "msg"];
let mut filtered = msg.clone();
filtered.retain(|key, _| MSG_KEYS.binary_search(&key).is_ok());
let mut buf = WriteBuf::new();
buf.0.extend_from_slice(b"BUG: ");
if filtered.write_json(&mut buf).is_ok() && buf.try_reserve(1).is_ok() {
buf.0.extend_from_slice(b"\n");
let _guard = LOG_WRITE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _ = write_all_to_fd(fd, &buf.0);
}
return;
}
let mut buf = WriteBuf::new();
let ok = if tty {
msg.write_json_pretty(&mut buf).is_ok()
} else {
msg.write_json(&mut buf).is_ok()
};
if ok && buf.try_reserve(1).is_ok() {
buf.0.extend_from_slice(b"\n");
let _guard = LOG_WRITE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _ = write_all_to_fd(fd, &buf.0);
}
}
pub(crate) static LOG_TTY: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
pub(crate) static LOG_FD: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(-42);
pub(crate) static LOG_MAIN_TID: OnceLock<ThreadId> = OnceLock::new();
pub(crate) fn log_fd() -> Option<BorrowedFd<'static>> {
let fd = LOG_FD.load(Ordering::Acquire);
if fd < 0 {
None
} else {
Some(unsafe { BorrowedFd::borrow_raw(fd) })
}
}
fn validate_log_fd<Fd: AsFd>(fd: Fd) -> Result<(), Errno> {
if !is_active_fd(&fd) {
return Err(Errno::EBADF);
}
if !is_writable_fd(&fd).unwrap_or(false) {
return Err(Errno::EBADFD);
}
set_append(&fd, true)?;
Ok(())
}
pub(crate) fn log_init_main() -> SydResult<()> {
LOG_MAIN_TID
.set(std::thread::current().id())
.or(Err(Errno::EBUSY.into()))
}
pub(crate) fn log_set_panic_hook() {
#[expect(clippy::cognitive_complexity)]
std::panic::set_hook(Box::new(|info| {
let this = std::thread::current();
let name = this.name().unwrap_or("?");
let err = match info.payload().downcast_ref::<&'static str>() {
Some(s) => *s,
None => match info.payload().downcast_ref::<String>() {
Some(s) => &**s,
None => "?",
},
};
let file = info.location().map(|l| l.file());
let line = info.location().map(|l| l.line());
let main = log_is_main(this.id());
let name = if main { "main" } else { name };
if main {
crate::alert!("ctx": "panic", "act": Action::Exit,
"name": name, "msg": err, "file": file, "line": line);
std::process::exit(101);
} else {
crate::crit!("ctx": "panic", "act": Action::Deny,
"name": name, "msg": err, "file": file, "line": line);
}
}));
}
pub(crate) fn log_is_main(tid: ThreadId) -> bool {
LOG_MAIN_TID
.get()
.map(|&main_tid| main_tid == tid)
.unwrap_or(false)
}
#[cfg(feature = "log")]
pub fn log_init(default_level: LogLevel, default_log_fd: Option<RawFd>) -> Result<(), Errno> {
use std::os::unix::ffi::OsStrExt;
use crate::syslog::{init_global_syslog, parse_loglevel};
let level = if let Some(val) = std::env::var_os(ENV_LOG) {
parse_loglevel(val.as_os_str().as_bytes(), default_level)
} else {
default_level
};
let fd = match std::env::var_os(ENV_LOG_FD) {
None => default_log_fd,
Some(val) => {
let fd = btoi::<RawFd>(val.as_os_str().as_bytes()).or(Err(Errno::EBADF))?;
if fd >= 0 {
let fd = unsafe { BorrowedFd::borrow_raw(fd) };
validate_log_fd(fd)?;
}
Some(fd)
}
};
if let Some(fd) = fd {
LOG_FD.store(fd, Ordering::Release);
}
let mut tty = std::env::var_os(ENV_FORCE_TTY).is_some();
if !tty {
if std::env::var_os(ENV_QUIET_TTY).is_none() {
let fd = fd.unwrap_or(libc::STDERR_FILENO);
tty = unsafe { libc::isatty(fd) } == 1;
} else {
tty = false;
}
}
LOG_TTY.store(tty, Ordering::Relaxed);
let mut logbuflen = 0usize;
let mut use_stack = true;
if let Some(var) = std::env::var_os(ENV_LOG_BUF_LEN) {
logbuflen = parse_size::Config::new()
.with_binary()
.parse_size(var.as_bytes())
.or(Err(Errno::EINVAL))?
.try_into()
.or(Err(Errno::EINVAL))?;
use_stack = false;
}
init_global_syslog(logbuflen, level, use_stack)?;
info!("ctx": "init", "op": "sing", "chapter": 24,
"msg": "Change return success. Going and coming without error. Action brings good fortune.");
Ok(())
}
#[cfg(feature = "log")]
pub fn log_init_simple(default_level: LogLevel) -> Result<(), Errno> {
use std::os::unix::ffi::OsStrExt;
use crate::syslog::{global_syslog, init_global_syslog, parse_loglevel};
let level = if let Some(val) = std::env::var_os(ENV_LOG) {
parse_loglevel(val.as_os_str().as_bytes(), default_level)
} else {
default_level
};
let fd = match std::env::var_os(ENV_LOG_FD) {
None => libc::STDERR_FILENO,
Some(val) => {
let fd = btoi::<RawFd>(val.as_os_str().as_bytes()).or(Err(Errno::EBADF))?;
if fd >= 0 {
let fd = unsafe { BorrowedFd::borrow_raw(fd) };
validate_log_fd(fd)?;
}
fd
}
};
LOG_FD.store(fd, Ordering::Release);
let mut tty = std::env::var_os(ENV_FORCE_TTY).is_some();
if !tty {
if std::env::var_os(ENV_QUIET_TTY).is_none() {
tty = unsafe { libc::isatty(fd) } == 1;
} else {
tty = false;
}
}
LOG_TTY.store(tty, Ordering::Relaxed);
init_global_syslog(0, level, true)?;
if let Some(sys) = global_syslog() {
sys.lock();
}
Ok(())
}
#[cfg(feature = "log")]
pub fn log(level: crate::syslog::LogLevel, timestamp: u64, mut msg: LogMap) {
let sys = if let Some(sys) = crate::syslog::global_syslog() {
sys
} else {
return; };
let msg_hash = {
let mut hasher = std::hash::DefaultHasher::new();
msg.hash(&mut hasher);
hasher.finish()
};
let add_context = level.as_u8() <= crate::syslog::LogLevel::Notice.as_u8();
let tty = LOG_TTY.load(Ordering::Relaxed);
enrich_msg(&mut msg, timestamp, add_context);
sys.write_log(level, &msg, tty, msg_hash);
}
#[cfg(not(feature = "log"))]
#[inline(always)]
pub fn log_init(_default_level: LogLevel, default_log_fd: Option<RawFd>) -> Result<(), Errno> {
use std::os::unix::ffi::OsStrExt;
let fd = match std::env::var_os(ENV_LOG_FD) {
None => default_log_fd,
Some(val) => {
let fd = btoi::<RawFd>(val.as_os_str().as_bytes()).or(Err(Errno::EBADF))?;
if fd >= 0 {
let fd = unsafe { BorrowedFd::borrow_raw(fd) };
validate_log_fd(fd)?;
}
Some(fd)
}
};
if let Some(fd) = fd {
LOG_FD.store(fd, Ordering::Release);
}
let mut tty = std::env::var_os(ENV_FORCE_TTY).is_some();
if !tty {
if std::env::var_os(ENV_QUIET_TTY).is_none() {
let fd = fd.unwrap_or(libc::STDERR_FILENO);
tty = unsafe { libc::isatty(fd) } == 1;
} else {
tty = false;
}
}
LOG_TTY.store(tty, Ordering::Relaxed);
info!("ctx": "init", "op": "sing", "chapter": 24,
"msg": "Change return success. Going and coming without error. Action brings good fortune.");
Ok(())
}
#[cfg(not(feature = "log"))]
#[inline(always)]
pub fn log_init_simple(default_level: LogLevel) -> Result<(), Errno> {
log_init(default_level, Some(libc::STDERR_FILENO))
}
#[cfg(not(feature = "log"))]
#[expect(clippy::cognitive_complexity)]
pub fn log(level: crate::syslog::LogLevel, timestamp: u64, mut msg: LogMap) {
let fd = if let Some(fd) = log_fd() {
fd
} else {
return; };
let msg_hash = {
let mut hasher = std::hash::DefaultHasher::new();
msg.hash(&mut hasher);
hasher.finish()
};
let add_context = level.as_u8() <= crate::syslog::LogLevel::Notice.as_u8();
let tty = LOG_TTY.load(Ordering::Relaxed) && !cfg!(feature = "kcov");
enrich_msg(&mut msg, timestamp, add_context);
let rlimit = global_log_rlimit();
let (allowed, suppressed) = rlimit.should_log(msg_hash);
if suppressed > 0 {
let mut smsg = LogMap::new();
let _ = smsg.try_insert("ctx", LogValue::Borrowed("log"));
let _ = smsg.try_insert("op", LogValue::Borrowed("rlimit"));
let _ = smsg.try_insert("suppressed", LogValue::from(suppressed));
emit_json_to_fd(fd, &smsg, tty, level);
}
if !allowed {
return;
}
emit_json_to_fd(fd, &msg, tty, level);
}
pub fn now() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
#[expect(clippy::cognitive_complexity)]
#[expect(clippy::cast_sign_loss)]
fn enrich_msg(msg: &mut LogMap, timestamp: u64, add_context: bool) {
let _ = msg.try_shift_insert(0, "syd", gettid().as_raw().into());
if let Ok(hid) = LogValue::try_from((getpid().as_raw() as u32).to_name()) {
let _ = msg.try_shift_insert(0, "hid", hid);
}
let _ = msg.try_shift_insert(0, "id", SydId::get_str().into());
if let Some(pid_v) = msg.remove("pid").and_then(|v| v.as_i64()) {
#[expect(clippy::cast_possible_truncation)]
let pid = Pid::from_raw(pid_v as libc::pid_t);
if pid.as_raw() != 0 {
if add_context {
if let Ok(cmd) = proc_cmdline(pid) {
if let Ok(v) = LogValue::try_from(cmd.to_string()) {
let _ = msg.try_insert("cmd", v);
}
}
} else if let Ok(cmd) = proc_comm(pid) {
if let Ok(v) = LogValue::try_from(cmd.to_string()) {
let _ = msg.try_insert("cmd", v);
}
}
if let Ok(dir) = proc_cwd(pid) {
if let Ok(v) = LogValue::try_from(dir.to_string()) {
let _ = msg.try_insert("cwd", v);
}
}
if add_context {
if let Ok(tty) = proc_tty(pid) {
if let Ok(v) = LogValue::try_from(tty.to_string()) {
let _ = msg.try_insert("tty", v);
}
}
}
}
let _ = msg.try_insert("pid", pid.as_raw().into());
}
if add_context {
let _ = msg.try_insert("uid", Uid::current().as_raw().into());
}
if let Ok(timestamp) = format_iso8601_value(timestamp) {
let _ = msg.try_insert("time", timestamp);
} else {
let _ = msg.try_insert("time", timestamp.into());
}
if let Some(src) = msg.remove("req") {
let _ = msg.try_insert("req", src);
}
if let Some(m) = msg.remove("msg") {
let _ = msg.try_insert("msg", m);
}
if let Some(tip) = msg.remove("tip") {
let _ = msg.try_insert("tip", tip);
}
msg.retain(|_, value| !value.is_null());
if cfg!(feature = "kcov") {
msg.retain(|key, _| {
const MSG_KEYS: &[&str] = &["err", "error", "file", "line", "msg"];
MSG_KEYS.binary_search(&key).is_ok()
});
}
}
fn format_iso8601_value(timestamp: u64) -> Result<LogValue, Errno> {
let t = i64::try_from(timestamp).or(Err(Errno::EINVAL))?;
let t: Tm = t.try_into()?;
let mut buf = [0u8; 24];
let n = {
let mut cursor = Cursor::new(&mut buf[..]);
write!(
cursor,
"\"{}T{}Z\"",
format_args!("{:04}{:02}{:02}", t.year(), t.month(), t.day()),
format_args!("{:02}{:02}{:02}", t.hour(), t.minute(), t.second()),
)
.or(Err(Errno::EOVERFLOW))?;
cursor.position()
};
let n = usize::try_from(n).or(Err(Errno::EOVERFLOW))?;
let mut v = Vec::new();
v.try_reserve(n).or(Err(Errno::ENOMEM))?;
v.extend_from_slice(&buf[..n]);
Ok(LogValue::Raw(v))
}
pub fn log_untrusted_buf(buf: &[u8]) -> (String, bool) {
if contains_ascii_unprintable(buf) {
(
hex_encode_lower(buf)
.ok()
.unwrap_or_else(|| "?".to_string()),
true,
)
} else if let Ok(s) = std::str::from_utf8(buf) {
(s.to_string(), false)
} else {
(
hex_encode_lower(buf)
.ok()
.unwrap_or_else(|| "?".to_string()),
true,
)
}
}
pub fn contains_ascii_unprintable(buf: &[u8]) -> bool {
buf.iter().any(|byte| !is_ascii_printable(*byte))
}
pub fn is_ascii_printable(byte: u8) -> bool {
(0x20..=0x7e).contains(&byte)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct Tm {
tm_year: i32, tm_mon: i32, tm_mday: i32, tm_wday: i32, tm_yday: i32, tm_hour: i32, tm_min: i32, tm_sec: i32, }
pub fn get_ioctl_log(
val: Ioctl,
arch: ScmpArch,
resolve: bool,
) -> Result<Option<Vec<IoctlName>>, Errno> {
if !resolve {
let mut vec = Vec::new();
vec.try_reserve(1).or(Err(Errno::ENOMEM))?;
vec.push(IoctlName::Val(val));
return Ok(Some(vec));
}
match IoctlMap::new(None, true).get_log(val, arch)? {
Some(names) => Ok(Some(names)),
None => {
let mut vec = Vec::new();
vec.try_reserve(1).or(Err(Errno::ENOMEM))?;
vec.push(IoctlName::Val(val));
Ok(Some(vec))
}
}
}
impl Tm {
pub fn year(&self) -> i32 {
self.tm_year.saturating_add(1900)
}
pub fn month(&self) -> i32 {
self.tm_mon.saturating_add(1)
}
pub fn day(&self) -> i32 {
self.tm_mday
}
pub fn weekday(&self) -> i32 {
self.tm_wday
}
pub fn hour(&self) -> i32 {
self.tm_hour
}
pub fn minute(&self) -> i32 {
self.tm_min
}
pub fn second(&self) -> i32 {
self.tm_sec
}
}
impl TryFrom<i64> for Tm {
type Error = Errno;
fn try_from(t: i64) -> Result<Self, Errno> {
const LEAPOCH: i64 = 951_868_800; const SECS_PER_DAY: i64 = 86_400;
const DAYS_PER_400Y: i64 = 146_097;
const DAYS_PER_100Y: i64 = 36_524;
const DAYS_PER_4Y: i64 = 1_461;
let limit_unit = 31_622_400i64; let low = i64::from(i32::MIN)
.checked_mul(limit_unit)
.ok_or(Errno::EOVERFLOW)?;
let high = i64::from(i32::MAX)
.checked_mul(limit_unit)
.ok_or(Errno::EOVERFLOW)?;
if t < low || t > high {
return Err(Errno::EOVERFLOW);
}
let secs = t.checked_sub(LEAPOCH).ok_or(Errno::EOVERFLOW)?;
let mut days = secs.checked_div(SECS_PER_DAY).ok_or(Errno::EOVERFLOW)?;
let mut remsecs = secs.checked_rem(SECS_PER_DAY).ok_or(Errno::EOVERFLOW)?;
if remsecs < 0 {
remsecs = remsecs.checked_add(SECS_PER_DAY).ok_or(Errno::EOVERFLOW)?;
days = days.checked_sub(1).ok_or(Errno::EOVERFLOW)?;
}
let mut wday = (3i64)
.checked_add(days)
.ok_or(Errno::EOVERFLOW)?
.checked_rem(7)
.ok_or(Errno::EOVERFLOW)?;
if wday < 0 {
wday = wday.checked_add(7).ok_or(Errno::EOVERFLOW)?;
}
let mut qc_cycles = days.checked_div(DAYS_PER_400Y).ok_or(Errno::EOVERFLOW)?;
let mut remdays = days.checked_rem(DAYS_PER_400Y).ok_or(Errno::EOVERFLOW)?;
if remdays < 0 {
remdays = remdays.checked_add(DAYS_PER_400Y).ok_or(Errno::EOVERFLOW)?;
qc_cycles = qc_cycles.checked_sub(1).ok_or(Errno::EOVERFLOW)?;
}
let mut c_cycles = remdays.checked_div(DAYS_PER_100Y).ok_or(Errno::EOVERFLOW)?;
if c_cycles == 4 {
c_cycles = c_cycles.checked_sub(1).ok_or(Errno::EOVERFLOW)?;
}
remdays = remdays
.checked_sub(
c_cycles
.checked_mul(DAYS_PER_100Y)
.ok_or(Errno::EOVERFLOW)?,
)
.ok_or(Errno::EOVERFLOW)?;
let mut q_cycles = remdays.checked_div(DAYS_PER_4Y).ok_or(Errno::EOVERFLOW)?;
if q_cycles == 25 {
q_cycles = q_cycles.checked_sub(1).ok_or(Errno::EOVERFLOW)?;
}
remdays = remdays
.checked_sub(q_cycles.checked_mul(DAYS_PER_4Y).ok_or(Errno::EOVERFLOW)?)
.ok_or(Errno::EOVERFLOW)?;
let mut remyears = remdays.checked_div(365).ok_or(Errno::EOVERFLOW)?;
if remyears == 4 {
remyears = remyears.checked_sub(1).ok_or(Errno::EOVERFLOW)?;
}
remdays = remdays
.checked_sub(remyears.checked_mul(365).ok_or(Errno::EOVERFLOW)?)
.ok_or(Errno::EOVERFLOW)?;
let leap = remyears == 0 && (q_cycles != 0 || c_cycles == 0);
let leap_i = if leap { 1i64 } else { 0i64 };
let mut yday = remdays
.checked_add(59)
.ok_or(Errno::EOVERFLOW)?
.checked_add(leap_i)
.ok_or(Errno::EOVERFLOW)?;
let yday_lim = 365i64.checked_add(leap_i).ok_or(Errno::EOVERFLOW)?;
if yday >= yday_lim {
yday = yday.checked_sub(yday_lim).ok_or(Errno::EOVERFLOW)?;
}
let years = remyears
.checked_add(4i64.checked_mul(q_cycles).ok_or(Errno::EOVERFLOW)?)
.ok_or(Errno::EOVERFLOW)?
.checked_add(100i64.checked_mul(c_cycles).ok_or(Errno::EOVERFLOW)?)
.ok_or(Errno::EOVERFLOW)?
.checked_add(400i64.checked_mul(qc_cycles).ok_or(Errno::EOVERFLOW)?)
.ok_or(Errno::EOVERFLOW)?;
let dim: [i64; 12] = [31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 31, 29];
let mut months = 0i64;
let mut rd = remdays;
while months < 12 {
let d = *dim
.get(usize::try_from(months).or(Err(Errno::EOVERFLOW))?)
.ok_or(Errno::EOVERFLOW)?;
if d > rd {
break;
}
rd = rd.checked_sub(d).ok_or(Errno::EOVERFLOW)?;
months = months.checked_add(1).ok_or(Errno::EOVERFLOW)?;
}
let mut years_adj = years;
let mut months_adj = months;
if months_adj >= 10 {
months_adj = months_adj.checked_sub(12).ok_or(Errno::EOVERFLOW)?;
years_adj = years_adj.checked_add(1).ok_or(Errno::EOVERFLOW)?;
}
let years_plus_100 = years_adj.checked_add(100).ok_or(Errno::EOVERFLOW)?;
if years_plus_100 > i64::from(i32::MAX) || years_plus_100 < i64::from(i32::MIN) {
return Err(Errno::EOVERFLOW);
}
let hour = remsecs.checked_div(3600).ok_or(Errno::EOVERFLOW)?;
let min = remsecs
.checked_div(60)
.ok_or(Errno::EOVERFLOW)?
.checked_rem(60)
.ok_or(Errno::EOVERFLOW)?;
let sec = remsecs.checked_rem(60).ok_or(Errno::EOVERFLOW)?;
Ok(Self {
tm_year: i32::try_from(years_plus_100).or(Err(Errno::EOVERFLOW))?,
tm_mon: i32::try_from(months_adj.checked_add(2).ok_or(Errno::EOVERFLOW)?)
.or(Err(Errno::EOVERFLOW))?,
tm_mday: i32::try_from(rd.checked_add(1).ok_or(Errno::EOVERFLOW)?)
.or(Err(Errno::EOVERFLOW))?,
tm_wday: i32::try_from(wday).or(Err(Errno::EOVERFLOW))?,
tm_yday: i32::try_from(yday).or(Err(Errno::EOVERFLOW))?,
tm_hour: i32::try_from(hour).or(Err(Errno::EOVERFLOW))?,
tm_min: i32::try_from(min).or(Err(Errno::EOVERFLOW))?,
tm_sec: i32::try_from(sec).or(Err(Errno::EOVERFLOW))?,
})
}
}
impl fmt::Display for Tm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let year = self.year();
let month = self.month();
let day = self.day();
let hour = self.hour();
let minute = self.minute();
let second = self.second();
write!(
f,
"{year:04}{month:02}{day:02}T{hour:02}{minute:02}{second:02}Z"
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_ascii_printable_1() {
assert!(is_ascii_printable(b' '));
assert!(is_ascii_printable(b'A'));
assert!(is_ascii_printable(b'z'));
assert!(is_ascii_printable(b'~'));
}
#[test]
fn test_is_ascii_printable_2() {
assert!(!is_ascii_printable(0x00));
assert!(!is_ascii_printable(0x1F));
assert!(!is_ascii_printable(0x7F));
assert!(!is_ascii_printable(0xFF));
}
#[test]
fn test_contains_ascii_unprintable_1() {
assert!(!contains_ascii_unprintable(b"hello world"));
}
#[test]
fn test_contains_ascii_unprintable_2() {
assert!(contains_ascii_unprintable(b"hello\x00world"));
}
#[test]
fn test_contains_ascii_unprintable_3() {
assert!(!contains_ascii_unprintable(b""));
}
#[test]
fn test_contains_ascii_unprintable_4() {
assert!(contains_ascii_unprintable(b"\x01"));
}
#[test]
fn test_contains_ascii_unprintable_5() {
assert!(contains_ascii_unprintable(b"\x7f"));
}
#[test]
fn test_log_untrusted_buf_1() {
let (s, is_hex) = log_untrusted_buf(b"hello");
assert_eq!(s, "hello");
assert!(!is_hex);
}
#[test]
fn test_log_untrusted_buf_2() {
let (s, is_hex) = log_untrusted_buf(b"\x00\x01");
assert!(is_hex);
assert_eq!(s, "0001");
}
#[test]
fn test_log_untrusted_buf_3() {
let (_, is_hex) = log_untrusted_buf(&[0x80, 0x81]);
assert!(is_hex);
}
#[test]
fn test_try_from_1() {
let tm: Tm = 0i64.try_into().unwrap();
assert_eq!(tm.year(), 1970);
assert_eq!(tm.month(), 1);
assert_eq!(tm.day(), 1);
assert_eq!(tm.hour(), 0);
assert_eq!(tm.minute(), 0);
assert_eq!(tm.second(), 0);
}
#[test]
fn test_try_from_2() {
let tm: Tm = 951_868_800i64.try_into().unwrap();
assert_eq!(tm.year(), 2000);
assert_eq!(tm.month(), 3);
assert_eq!(tm.day(), 1);
}
#[test]
fn test_try_from_3() {
let tm: Tm = 1_704_067_200i64.try_into().unwrap();
assert_eq!(tm.year(), 2024);
assert_eq!(tm.month(), 1);
assert_eq!(tm.day(), 1);
}
#[test]
fn test_display_1() {
let tm: Tm = 0i64.try_into().unwrap();
let s = format!("{tm}");
assert_eq!(s, "19700101T000000Z");
}
#[test]
fn test_display_2() {
let tm: Tm = 1_704_067_200i64.try_into().unwrap();
let s = format!("{tm}");
assert!(s.starts_with("2024"));
assert!(s.ends_with('Z'));
}
#[test]
fn test_year_1() {
let tm: Tm = 0i64.try_into().unwrap();
assert_eq!(tm.year(), 1970);
}
#[test]
fn test_weekday_1() {
let tm: Tm = 0i64.try_into().unwrap();
assert_eq!(tm.weekday(), 4);
}
}