#![forbid(unsafe_code)]
#![warn(missing_debug_implementations)]
pub mod aux_tables;
pub mod cli;
pub mod compare;
pub mod compile;
pub mod diagnostics;
pub mod doctor;
pub mod error;
pub mod fs;
pub mod hash;
pub(crate) mod json;
pub mod limits;
pub mod manifest;
pub mod model;
pub mod release_diff;
pub mod report;
pub mod semantic_witness;
pub mod size_report;
pub mod source;
pub mod structural;
pub mod tzif;
pub mod vendor_oracle;
pub use diagnostics::{
Diagnostic, DiagnosticCode, DiagnosticLayer, DiagnosticSpanPrecision, DiagnosticVerbosity,
Severity,
};
pub use error::{Error, Result};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LinkMode {
#[default]
Copy,
Symlink,
}
impl LinkMode {
pub fn as_str(self) -> &'static str {
match self {
LinkMode::Copy => "copy",
LinkMode::Symlink => "symlink",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum EmitStyle {
#[default]
Default,
ZicSlim,
ZicFat,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EmitOptions {
pub style: EmitStyle,
pub redundant_until: Option<i64>,
pub range: Option<RangeSpec>,
}
impl From<EmitStyle> for EmitOptions {
fn from(style: EmitStyle) -> Self {
EmitOptions {
style,
redundant_until: None,
range: None,
}
}
}
impl Default for EmitOptions {
fn default() -> Self {
EmitStyle::Default.into()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RangeSpec {
pub lo: Option<i64>,
pub hi: Option<i64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum UnsupportedPolicy {
#[default]
Error,
WarnAndSkipZone,
}
#[derive(Debug, Clone)]
pub enum ZoneSelection {
One(String),
Many(Vec<String>),
AllSupported,
}
#[derive(Debug, Clone)]
pub struct CompileConfig {
pub input_paths: Vec<PathBuf>,
pub output_dir: PathBuf,
pub zones: ZoneSelection,
pub link_mode: LinkMode,
pub overwrite: bool,
pub unsupported_policy: UnsupportedPolicy,
pub transition_limit: usize,
pub emit_style: EmitStyle,
pub no_create_dirs: bool,
pub localtime: Option<String>,
pub localtime_name: Option<String>,
pub file_mode: Option<u32>,
pub redundant_until: Option<i64>,
pub range: Option<RangeSpec>,
pub leaps: Option<model::LeapTable>,
}
pub const DEFAULT_TRANSITION_LIMIT: usize = 100_000;
#[derive(Debug, Clone)]
pub struct ZoneReport {
pub name: String,
pub output_path: PathBuf,
pub tzif_version: u8,
pub transition_count: usize,
}
#[derive(Debug, Clone)]
pub struct LinkReport {
pub link_name: String,
pub target: String,
pub mode: LinkMode,
}
#[derive(Debug, Default, Clone)]
pub struct CompileReport {
pub zones_compiled: Vec<ZoneReport>,
pub links_written: Vec<LinkReport>,
pub diagnostics: Vec<Diagnostic>,
}
pub fn load_database(paths: &[PathBuf]) -> Result<model::Database> {
let limits = limits::ResourceLimits::default();
let files = collect_source_files(paths)?;
let mut db = model::Database::default();
for f in &files {
let bytes = std::fs::read(f).map_err(|e| Error::io(f, e))?;
limits.check_source_bytes(bytes.len(), f)?;
source::parse_into(&bytes, f, &mut db)?;
}
limits.enforce(&db)?;
Ok(db)
}
pub fn collect_source_files(paths: &[PathBuf]) -> Result<Vec<PathBuf>> {
let mut files: Vec<PathBuf> = Vec::new();
for p in paths {
if p.is_dir() {
let mut entries: Vec<PathBuf> = std::fs::read_dir(p)
.map_err(|e| Error::io(p, e))?
.filter_map(|e| e.ok().map(|e| e.path()))
.filter(|p| p.is_file())
.collect();
entries.sort();
files.extend(entries);
} else {
files.push(p.clone());
}
}
Ok(files)
}
pub fn compile_zone(db: &model::Database, name: &str) -> Result<tzif::TzifData> {
compile::compile_zone(db, name)
}
pub fn compile_zone_styled(
db: &model::Database,
name: &str,
opts: impl Into<EmitOptions>,
) -> Result<tzif::TzifData> {
compile::compile_zone_styled(db, name, opts.into())
}
pub fn compile_zone_to_bytes(db: &model::Database, name: &str) -> Result<Vec<u8>> {
let data = compile_zone(db, name)?;
tzif::write_bytes(&data)
}
pub fn compile_zone_to_bytes_styled(
db: &model::Database,
name: &str,
opts: impl Into<EmitOptions>,
) -> Result<Vec<u8>> {
let data = compile_zone_styled(db, name, opts.into())?;
tzif::write_bytes(&data)
}
pub fn resolve_link_target<'a>(db: &'a model::Database, name: &'a str) -> Result<&'a str> {
let mut current = name;
let mut visited: Vec<&'a str> = Vec::new();
loop {
if db.zones.iter().any(|z| z.name == current) {
return Ok(current);
}
if visited.len() >= limits::DEFAULT_LINK_CHAIN_DEPTH_MAX {
return Err(Error::config(format!(
"link chain for {name:?} exceeds the zic-rs depth limit of {} hops",
limits::DEFAULT_LINK_CHAIN_DEPTH_MAX
)));
}
if visited.contains(¤t) {
visited.push(current);
return Err(Error::message(format!(
"link chain for {name:?} forms a cycle: {}",
visited.join(" -> ")
)));
}
match db.links.iter().find(|l| l.link_name == current) {
Some(l) => {
visited.push(current);
current = &l.target;
}
None => {
return Err(Error::message(format!(
"link target {current:?} does not name a zone or link (resolving {name:?})"
)))
}
}
}
}
pub fn is_contained(root: &Path, candidate: &Path) -> bool {
fs::output_tree::is_contained(root, candidate)
}