#![forbid(unsafe_code)]
pub trait Host {
fn read_line(&mut self) -> Option<String>;
fn err(&mut self, s: &str);
fn out(&mut self, s: &str);
fn read_table(&self, basename: &str) -> Option<String>;
fn run_date(&self, tz: &str) -> Option<String>;
fn run_date_fmt(&self, tz: &str, fmt: &str) -> Option<String>;
fn zone_readable(&self, tz: &str) -> bool;
fn stdout_is_tty(&self) -> bool;
}
#[derive(Clone, Debug)]
pub struct Options {
pub coord: Option<String>,
pub location_limit: usize,
pub zonetabtype: String,
pub tzdir: String,
pub pkgversion: String,
pub tzversion: String,
pub argv0: String,
}
impl Default for Options {
fn default() -> Self {
Options {
coord: None,
location_limit: 10,
zonetabtype: "zone1970".to_string(),
tzdir: ".".to_string(),
pkgversion: "(tzcode) ".to_string(),
tzversion: env!("CARGO_PKG_VERSION").to_string(),
argv0: "tzselect".to_string(),
}
}
}
pub fn usage(o: &Options) -> String {
format!(
"Usage: tzselect [--version] [--help] [-c COORD] [-n LIMIT]
Select a timezone interactively.
Options:
-c COORD
Instead of asking for continent and then country and then city,
ask for selection from time zones whose largest cities
are closest to the location with geographical coordinates COORD.
COORD should use ISO 6709 notation, for example, '-c +4852+00220'
for Paris (in degrees and minutes, North and East), or
'-c -35-058' for Buenos Aires (in degrees, South and West).
-n LIMIT
Display at most LIMIT locations when -c is used (default {}).
--version
Output version information.
--help
Output this help.
Report bugs to tz@iana.org.",
o.location_limit
)
}
struct Selection {
tz: String,
time: String,
country_result: String,
region: String,
needs_zone_check: bool,
}
fn say_err(h: &mut dyn Host, o: &Options, msg: &str) {
h.err(&format!("{}: {}\n", o.argv0, msg));
}
fn say_err_raw(h: &mut dyn Host, msg: &str) {
h.err(msg);
h.err("\n");
}
pub fn run(o: &Options, h: &mut dyn Host) -> i32 {
let country_table = match h.read_table(&format!("{}/iso3166.tab", o.tzdir)) {
Some(s) => s,
None => {
say_err(h, o, "time zone files are not set up correctly");
return 1;
}
};
let zonetabtype_table = match h.read_table(&format!("{}/{}.tab", o.tzdir, o.zonetabtype)) {
Some(s) => s,
None => {
say_err(h, o, "time zone files are not set up correctly");
return 1;
}
};
let mut zonenow_table: Option<String> = None;
let mut coord = o.coord.clone();
loop {
h.err("Please identify a location so that time zone rules can be set correctly.\n");
let continent = match &coord {
Some(c) if !c.is_empty() => "coord".to_string(),
_ => match ask_continent(o, h, &zonetabtype_table) {
Some(c) => c,
None => return 1,
},
};
let mut working_table = zonetabtype_table.clone();
if o.zonetabtype != "zonenow" && continent == "now" {
if zonenow_table.is_none() {
zonenow_table = h.read_table(&format!("{}/zonenow.tab", o.tzdir));
}
if let Some(t) = &zonenow_table {
working_table = t.clone();
}
}
let sel = match dispatch(o, h, &continent, &mut coord, &country_table, &working_table) {
Dispatch::Exit(code) => return code,
Dispatch::Sel(s) => s,
};
let tz_for_date = if sel.needs_zone_check {
let path = format!("{}/{}", o.tzdir, sel.tz);
if !h.zone_readable(&path) {
say_err(h, o, "time zone files are not set up correctly");
return 1;
}
path
} else {
sel.tz.clone()
};
finish(h, &sel, &coord, &tz_for_date);
match doselect(o, h, &["Yes".to_string(), "No".to_string()]) {
None => return 1,
Some(ok) if ok == "Yes" => {
permanent_hint(o, h, &sel.tz);
h.out(&sel.tz);
h.out("\n");
return 0;
}
Some(_) => {
coord = None; continue;
}
}
}
}
enum Dispatch {
Sel(Selection),
Exit(i32),
}
fn dispatch(
o: &Options,
h: &mut dyn Host,
continent: &str,
coord: &mut Option<String>,
country_table: &str,
zone_table: &str,
) -> Dispatch {
match continent {
"TZ" => dispatch_tz(o, h),
"coord" => dispatch_coord(o, h, coord, country_table, zone_table),
"now" | "time" => dispatch_time(o, h, country_table, zone_table),
_ => dispatch_normal(o, h, continent, country_table, zone_table),
}
}
fn dispatch_tz(o: &Options, h: &mut dyn Host) -> Dispatch {
let tz = loop {
h.err("Please enter the desired value of the TZ environment variable.\n");
h.err("For example, AEST-10 is abbreviated AEST and is 10 hours\n");
h.err("ahead (east) of Greenwich, with no daylight saving time.\n");
let entered = match h.read_line() {
Some(s) => s,
None => return Dispatch::Exit(1),
};
if posix_tz_valid(&entered) {
break entered;
}
say_err_raw(
h,
&format!("'{entered}' is not a conforming POSIX proleptic TZ string."),
);
};
let _ = o;
Dispatch::Sel(Selection {
tz,
time: String::new(),
country_result: String::new(),
region: String::new(),
needs_zone_check: false,
})
}
fn dispatch_coord(
o: &Options,
h: &mut dyn Host,
coord: &mut Option<String>,
country_table: &str,
zone_table: &str,
) -> Dispatch {
let c = match coord {
Some(c) if !c.is_empty() => c.clone(),
_ => {
h.err("Please enter coordinates in ISO 6709 notation.\n");
h.err("For example, +4042-07403 stands for\n");
h.err("40 degrees 42 minutes north, 74 degrees 3 minutes west.\n");
match h.read_line() {
Some(s) => {
*coord = Some(s.clone());
s
}
None => String::new(),
}
}
};
let mut rows = output_distances(&c, country_table, zone_table);
rows.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
rows.truncate(o.location_limit);
let distance_table: Vec<String> = rows
.iter()
.map(|(d, line)| format!("{}\t{}", fmt_g(*d), line))
.collect();
let regions: Vec<String> = distance_table
.iter()
.map(|l| l.rsplit('\t').next().unwrap_or("").to_string())
.collect();
h.err("Please select one of the following timezones,\n");
say_err_raw(
h,
&format!("listed roughly in increasing order of distance from {c}."),
);
let region = match doselect(o, h, ®ions) {
Some(s) => s,
None => return Dispatch::Exit(1),
};
let mut tz = String::new();
for l in &distance_table {
let f: Vec<&str> = l.split('\t').collect();
if f.last().copied().unwrap_or("") == region {
tz = f.get(3).copied().unwrap_or("").to_string();
break;
}
}
Dispatch::Sel(Selection {
tz,
time: String::new(),
country_result: String::new(),
region,
needs_zone_check: true,
})
}
fn dispatch_time(o: &Options, h: &mut dyn Host, country_table: &str, zone_table: &str) -> Dispatch {
let time_table = build_time_table(h, zone_table);
let new_minute = h.run_date_fmt("UTC0", "%a %b %d %H:%M").unwrap_or_default();
say_err_raw(
h,
&format!("The system says Universal Time is {new_minute}."),
);
h.err("Assuming that's correct, what is the local time?\n");
let sorted = sort_time_table(&time_table);
let mut menu: Vec<String> = Vec::new();
let mut last = String::new();
for l in &sorted {
let key = time_key(l);
if key != last {
menu.push(key.clone());
last = key;
}
}
let time = match doselect(o, h, &menu) {
Some(s) => s,
None => return Dispatch::Exit(1),
};
let mut zt = String::new();
for l in &time_table {
if time_key(l) == time {
if let Some(pos) = l.find('\t') {
zt.push_str(&l[pos + 1..]);
zt.push('\n');
}
}
}
let countries = country_menu(o, h, "^", country_table, &zt);
let (cr, country) = match pick_country(o, h, &countries) {
Some(v) => v,
None => return Dispatch::Exit(1),
};
let regions = regions_for_country(&country, country_table, &zt);
let mut region = String::new();
if regions.len() > 1 {
h.err("Please select one of the following timezones.\n");
match doselect(o, h, ®ions) {
Some(s) => region = s,
None => return Dispatch::Exit(1),
}
}
let tz = derive_tz(&country, ®ion, country_table, &zt);
Dispatch::Sel(Selection {
tz,
time,
country_result: cr.unwrap_or_default(),
region,
needs_zone_check: true,
})
}
fn dispatch_normal(
o: &Options,
h: &mut dyn Host,
continent: &str,
country_table: &str,
zone_table: &str,
) -> Dispatch {
let continent_re = format!("^{continent}/");
let countries = country_menu(o, h, &continent_re, country_table, zone_table);
let (cr, country) = match pick_country(o, h, &countries) {
Some(v) => v,
None => return Dispatch::Exit(1),
};
let regions = regions_for_country(&country, country_table, zone_table);
let mut region = String::new();
if regions.len() > 1 {
h.err("Please select one of the following timezones.\n");
match doselect(o, h, ®ions) {
Some(s) => region = s,
None => return Dispatch::Exit(1),
}
}
let tz = derive_tz(&country, ®ion, country_table, zone_table);
Dispatch::Sel(Selection {
tz,
time: String::new(),
country_result: cr.unwrap_or_default(),
region,
needs_zone_check: true,
})
}
fn finish(h: &mut dyn Host, sel: &Selection, coord: &Option<String>, tz_for_date: &str) {
let mut extra_info = String::new();
for _ in 0..8 {
let tzdate = h.run_date(tz_for_date).unwrap_or_default();
let utdate = h.run_date("UTC0").unwrap_or_default();
if secs_match(&tzdate, &utdate) {
extra_info =
format!("\nSelected time is now:\t{tzdate}.\nUniversal Time is now:\t{utdate}.");
break;
}
}
h.err("\n");
h.err("Based on the following information:\n");
h.err("\n");
let coord_s = coord.clone().unwrap_or_default();
let nz = |s: &str| !s.is_empty();
let summary = match (
nz(&sel.time),
nz(&sel.country_result),
nz(&sel.region),
nz(&coord_s),
) {
(true, true, true, false) => {
format!("\t{}\n\t{}\n\t{}", sel.time, sel.country_result, sel.region)
}
(true, true, false, false) | (true, false, true, false) => {
format!("\t{}\n\t{}{}", sel.time, sel.country_result, sel.region)
}
(true, false, false, false) => format!("\t{}", sel.time),
(false, true, true, false) => format!("\t{}\n\t{}", sel.country_result, sel.region),
(false, true, false, false) => format!("\t{}", sel.country_result),
(false, false, true, true) => format!("\tcoord {}\n\t{}", coord_s, sel.region),
(false, false, false, true) => format!("\tcoord {coord_s}"),
_ => format!("\tTZ='{}'", sel.tz),
};
say_err_raw(h, &summary);
h.err("\n");
say_err_raw(h, &format!("TZ='{}' will be used.{extra_info}", sel.tz));
h.err("Is the above information OK?\n");
}
fn permanent_hint(o: &Options, h: &mut dyn Host, tz: &str) {
if !h.stdout_is_tty() {
return;
}
let line = format!("export TZ='{tz}'");
h.err(&format!(
"\nYou can make this change permanent for yourself by appending the line\n\t{line}\nto the file '.profile' in your home directory; then log out and log in again.\n\nHere is that TZ value again, this time on standard output so that you\ncan use the {} command in shell scripts:\n",
o.argv0
));
}
include!("logic.rs");
#[cfg(feature = "fuzzing")]
#[doc(hidden)]
pub mod fuzz {
use super::*;
struct FuzzHost {
lines: std::vec::IntoIter<String>,
tables: std::collections::HashMap<String, String>,
}
impl Host for FuzzHost {
fn read_line(&mut self) -> Option<String> {
self.lines.next()
}
fn err(&mut self, _s: &str) {}
fn out(&mut self, _s: &str) {}
fn read_table(&self, path: &str) -> Option<String> {
let base = path.rsplit('/').next().unwrap_or(path);
self.tables.get(base).cloned()
}
fn run_date(&self, _tz: &str) -> Option<String> {
Some("Mon Jan 1 00:00:00 UTC 2024".to_string())
}
fn run_date_fmt(&self, _tz: &str, _fmt: &str) -> Option<String> {
Some("2024 01 01 00:00 Mon Jan".to_string())
}
fn zone_readable(&self, _path: &str) -> bool {
true
}
fn stdout_is_tty(&self) -> bool {
false
}
}
pub fn __fuzz_run(input: &str, iso3166: &str, zone1970: &str) {
let mut tables = std::collections::HashMap::new();
tables.insert("iso3166.tab".to_string(), iso3166.to_string());
tables.insert("zone1970.tab".to_string(), zone1970.to_string());
tables.insert("zonenow.tab".to_string(), zone1970.to_string());
let mut h = FuzzHost {
lines: input
.lines()
.map(|s| s.to_string())
.collect::<Vec<_>>()
.into_iter(),
tables,
};
let _ = run(&Options::default(), &mut h);
}
pub fn __fuzz_posix_tz(s: &str) {
let _ = posix_tz_valid(s);
}
}
#[cfg(kani)]
mod kani_harness {
#[kani::proof]
fn menu_index_guard_is_sound() {
let len: usize = kani::any();
let n: usize = kani::any();
kani::assume(len <= 4096);
if (1..=len).contains(&n) {
assert!(n >= 1);
assert!(n - 1 < len);
}
}
}