1#![forbid(unsafe_code)]
33
34pub trait Host {
37 fn read_line(&mut self) -> Option<String>;
40 fn err(&mut self, s: &str);
42 fn out(&mut self, s: &str);
44 fn read_table(&self, basename: &str) -> Option<String>;
46 fn run_date(&self, tz: &str) -> Option<String>;
48 fn run_date_fmt(&self, tz: &str, fmt: &str) -> Option<String>;
50 fn zone_readable(&self, tz: &str) -> bool;
52 fn stdout_is_tty(&self) -> bool;
54}
55
56#[derive(Clone, Debug)]
58pub struct Options {
59 pub coord: Option<String>,
61 pub location_limit: usize,
63 pub zonetabtype: String,
65 pub tzdir: String,
67 pub pkgversion: String,
69 pub tzversion: String,
71 pub argv0: String,
73}
74
75impl Default for Options {
76 fn default() -> Self {
77 Options {
78 coord: None,
79 location_limit: 10,
80 zonetabtype: "zone1970".to_string(),
81 tzdir: ".".to_string(),
82 pkgversion: "(tzcode) ".to_string(),
83 tzversion: env!("CARGO_PKG_VERSION").to_string(),
84 argv0: "tzselect".to_string(),
85 }
86 }
87}
88
89pub fn usage(o: &Options) -> String {
91 format!(
92 "Usage: tzselect [--version] [--help] [-c COORD] [-n LIMIT]
93Select a timezone interactively.
94
95Options:
96
97 -c COORD
98 Instead of asking for continent and then country and then city,
99 ask for selection from time zones whose largest cities
100 are closest to the location with geographical coordinates COORD.
101 COORD should use ISO 6709 notation, for example, '-c +4852+00220'
102 for Paris (in degrees and minutes, North and East), or
103 '-c -35-058' for Buenos Aires (in degrees, South and West).
104
105 -n LIMIT
106 Display at most LIMIT locations when -c is used (default {}).
107
108 --version
109 Output version information.
110
111 --help
112 Output this help.
113
114Report bugs to tz@iana.org.",
115 o.location_limit
116 )
117}
118
119struct Selection {
121 tz: String,
122 time: String,
123 country_result: String,
124 region: String,
125 needs_zone_check: bool,
127}
128
129fn say_err(h: &mut dyn Host, o: &Options, msg: &str) {
131 h.err(&format!("{}: {}\n", o.argv0, msg));
132}
133fn say_err_raw(h: &mut dyn Host, msg: &str) {
135 h.err(msg);
136 h.err("\n");
137}
138
139pub fn run(o: &Options, h: &mut dyn Host) -> i32 {
142 let country_table = match h.read_table(&format!("{}/iso3166.tab", o.tzdir)) {
143 Some(s) => s,
144 None => {
145 say_err(h, o, "time zone files are not set up correctly");
146 return 1;
147 }
148 };
149 let zonetabtype_table = match h.read_table(&format!("{}/{}.tab", o.tzdir, o.zonetabtype)) {
150 Some(s) => s,
151 None => {
152 say_err(h, o, "time zone files are not set up correctly");
153 return 1;
154 }
155 };
156 let mut zonenow_table: Option<String> = None;
157 let mut coord = o.coord.clone();
158
159 loop {
160 h.err("Please identify a location so that time zone rules can be set correctly.\n");
161
162 let continent = match &coord {
163 Some(c) if !c.is_empty() => "coord".to_string(),
164 _ => match ask_continent(o, h, &zonetabtype_table) {
165 Some(c) => c,
166 None => return 1,
167 },
168 };
169
170 let mut working_table = zonetabtype_table.clone();
172 if o.zonetabtype != "zonenow" && continent == "now" {
173 if zonenow_table.is_none() {
174 zonenow_table = h.read_table(&format!("{}/zonenow.tab", o.tzdir));
175 }
176 if let Some(t) = &zonenow_table {
177 working_table = t.clone();
178 }
179 }
180
181 let sel = match dispatch(o, h, &continent, &mut coord, &country_table, &working_table) {
182 Dispatch::Exit(code) => return code,
183 Dispatch::Sel(s) => s,
184 };
185
186 let tz_for_date = if sel.needs_zone_check {
189 let path = format!("{}/{}", o.tzdir, sel.tz);
190 if !h.zone_readable(&path) {
191 say_err(h, o, "time zone files are not set up correctly");
192 return 1;
193 }
194 path
195 } else {
196 sel.tz.clone()
197 };
198
199 finish(h, &sel, &coord, &tz_for_date);
200
201 match doselect(o, h, &["Yes".to_string(), "No".to_string()]) {
202 None => return 1,
203 Some(ok) if ok == "Yes" => {
204 permanent_hint(o, h, &sel.tz);
205 h.out(&sel.tz);
206 h.out("\n");
207 return 0;
208 }
209 Some(_) => {
210 coord = None; continue;
212 }
213 }
214 }
215}
216
217enum Dispatch {
219 Sel(Selection),
220 Exit(i32),
221}
222
223fn dispatch(
224 o: &Options,
225 h: &mut dyn Host,
226 continent: &str,
227 coord: &mut Option<String>,
228 country_table: &str,
229 zone_table: &str,
230) -> Dispatch {
231 match continent {
232 "TZ" => dispatch_tz(o, h),
233 "coord" => dispatch_coord(o, h, coord, country_table, zone_table),
234 "now" | "time" => dispatch_time(o, h, country_table, zone_table),
235 _ => dispatch_normal(o, h, continent, country_table, zone_table),
236 }
237}
238
239fn dispatch_tz(o: &Options, h: &mut dyn Host) -> Dispatch {
241 let tz = loop {
242 h.err("Please enter the desired value of the TZ environment variable.\n");
243 h.err("For example, AEST-10 is abbreviated AEST and is 10 hours\n");
244 h.err("ahead (east) of Greenwich, with no daylight saving time.\n");
245 let entered = match h.read_line() {
246 Some(s) => s,
247 None => return Dispatch::Exit(1),
248 };
249 if posix_tz_valid(&entered) {
250 break entered;
251 }
252 say_err_raw(
253 h,
254 &format!("'{entered}' is not a conforming POSIX proleptic TZ string."),
255 );
256 };
257 let _ = o;
258 Dispatch::Sel(Selection {
259 tz,
260 time: String::new(),
261 country_result: String::new(),
262 region: String::new(),
263 needs_zone_check: false,
264 })
265}
266
267fn dispatch_coord(
269 o: &Options,
270 h: &mut dyn Host,
271 coord: &mut Option<String>,
272 country_table: &str,
273 zone_table: &str,
274) -> Dispatch {
275 let c = match coord {
276 Some(c) if !c.is_empty() => c.clone(),
277 _ => {
278 h.err("Please enter coordinates in ISO 6709 notation.\n");
279 h.err("For example, +4042-07403 stands for\n");
280 h.err("40 degrees 42 minutes north, 74 degrees 3 minutes west.\n");
281 match h.read_line() {
285 Some(s) => {
286 *coord = Some(s.clone());
287 s
288 }
289 None => String::new(),
290 }
291 }
292 };
293 let mut rows = output_distances(&c, country_table, zone_table);
294 rows.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
295 rows.truncate(o.location_limit);
296 let distance_table: Vec<String> = rows
297 .iter()
298 .map(|(d, line)| format!("{}\t{}", fmt_g(*d), line))
299 .collect();
300 let regions: Vec<String> = distance_table
301 .iter()
302 .map(|l| l.rsplit('\t').next().unwrap_or("").to_string())
303 .collect();
304 h.err("Please select one of the following timezones,\n");
305 say_err_raw(
306 h,
307 &format!("listed roughly in increasing order of distance from {c}."),
308 );
309 let region = match doselect(o, h, ®ions) {
310 Some(s) => s,
311 None => return Dispatch::Exit(1),
312 };
313 let mut tz = String::new();
314 for l in &distance_table {
315 let f: Vec<&str> = l.split('\t').collect();
316 if f.last().copied().unwrap_or("") == region {
317 tz = f.get(3).copied().unwrap_or("").to_string();
318 break;
319 }
320 }
321 Dispatch::Sel(Selection {
322 tz,
323 time: String::new(),
324 country_result: String::new(),
325 region,
326 needs_zone_check: true,
327 })
328}
329
330fn dispatch_time(o: &Options, h: &mut dyn Host, country_table: &str, zone_table: &str) -> Dispatch {
332 let time_table = build_time_table(h, zone_table);
333 let new_minute = h.run_date_fmt("UTC0", "%a %b %d %H:%M").unwrap_or_default();
334 say_err_raw(
335 h,
336 &format!("The system says Universal Time is {new_minute}."),
337 );
338 h.err("Assuming that's correct, what is the local time?\n");
339 let sorted = sort_time_table(&time_table);
340 let mut menu: Vec<String> = Vec::new();
341 let mut last = String::new();
342 for l in &sorted {
343 let key = time_key(l);
344 if key != last {
345 menu.push(key.clone());
346 last = key;
347 }
348 }
349 let time = match doselect(o, h, &menu) {
350 Some(s) => s,
351 None => return Dispatch::Exit(1),
352 };
353 let mut zt = String::new();
355 for l in &time_table {
356 if time_key(l) == time {
357 if let Some(pos) = l.find('\t') {
358 zt.push_str(&l[pos + 1..]);
359 zt.push('\n');
360 }
361 }
362 }
363 let countries = country_menu(o, h, "^", country_table, &zt);
364 let (cr, country) = match pick_country(o, h, &countries) {
365 Some(v) => v,
366 None => return Dispatch::Exit(1),
367 };
368 let regions = regions_for_country(&country, country_table, &zt);
369 let mut region = String::new();
370 if regions.len() > 1 {
371 h.err("Please select one of the following timezones.\n");
372 match doselect(o, h, ®ions) {
373 Some(s) => region = s,
374 None => return Dispatch::Exit(1),
375 }
376 }
377 let tz = derive_tz(&country, ®ion, country_table, &zt);
378 Dispatch::Sel(Selection {
379 tz,
380 time,
381 country_result: cr.unwrap_or_default(),
382 region,
383 needs_zone_check: true,
384 })
385}
386
387fn dispatch_normal(
389 o: &Options,
390 h: &mut dyn Host,
391 continent: &str,
392 country_table: &str,
393 zone_table: &str,
394) -> Dispatch {
395 let continent_re = format!("^{continent}/");
396 let countries = country_menu(o, h, &continent_re, country_table, zone_table);
397 let (cr, country) = match pick_country(o, h, &countries) {
398 Some(v) => v,
399 None => return Dispatch::Exit(1),
400 };
401 let regions = regions_for_country(&country, country_table, zone_table);
402 let mut region = String::new();
403 if regions.len() > 1 {
404 h.err("Please select one of the following timezones.\n");
405 match doselect(o, h, ®ions) {
406 Some(s) => region = s,
407 None => return Dispatch::Exit(1),
408 }
409 }
410 let tz = derive_tz(&country, ®ion, country_table, zone_table);
411 Dispatch::Sel(Selection {
412 tz,
413 time: String::new(),
414 country_result: cr.unwrap_or_default(),
415 region,
416 needs_zone_check: true,
417 })
418}
419
420fn finish(h: &mut dyn Host, sel: &Selection, coord: &Option<String>, tz_for_date: &str) {
423 let mut extra_info = String::new();
424 for _ in 0..8 {
425 let tzdate = h.run_date(tz_for_date).unwrap_or_default();
426 let utdate = h.run_date("UTC0").unwrap_or_default();
427 if secs_match(&tzdate, &utdate) {
428 extra_info =
429 format!("\nSelected time is now:\t{tzdate}.\nUniversal Time is now:\t{utdate}.");
430 break;
431 }
432 }
433
434 h.err("\n");
435 h.err("Based on the following information:\n");
436 h.err("\n");
437
438 let coord_s = coord.clone().unwrap_or_default();
439 let nz = |s: &str| !s.is_empty();
440 let summary = match (
441 nz(&sel.time),
442 nz(&sel.country_result),
443 nz(&sel.region),
444 nz(&coord_s),
445 ) {
446 (true, true, true, false) => {
447 format!("\t{}\n\t{}\n\t{}", sel.time, sel.country_result, sel.region)
448 }
449 (true, true, false, false) | (true, false, true, false) => {
450 format!("\t{}\n\t{}{}", sel.time, sel.country_result, sel.region)
451 }
452 (true, false, false, false) => format!("\t{}", sel.time),
453 (false, true, true, false) => format!("\t{}\n\t{}", sel.country_result, sel.region),
454 (false, true, false, false) => format!("\t{}", sel.country_result),
455 (false, false, true, true) => format!("\tcoord {}\n\t{}", coord_s, sel.region),
456 (false, false, false, true) => format!("\tcoord {coord_s}"),
457 _ => format!("\tTZ='{}'", sel.tz),
458 };
459 say_err_raw(h, &summary);
460 h.err("\n");
461 say_err_raw(h, &format!("TZ='{}' will be used.{extra_info}", sel.tz));
462 h.err("Is the above information OK?\n");
463}
464
465fn permanent_hint(o: &Options, h: &mut dyn Host, tz: &str) {
467 if !h.stdout_is_tty() {
468 return;
469 }
470 let line = format!("export TZ='{tz}'");
471 h.err(&format!(
472 "\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",
473 o.argv0
474 ));
475}
476
477include!("logic.rs");
478
479#[cfg(feature = "fuzzing")]
481#[doc(hidden)]
482pub mod fuzz {
483 use super::*;
484
485 struct FuzzHost {
488 lines: std::vec::IntoIter<String>,
489 tables: std::collections::HashMap<String, String>,
490 }
491 impl Host for FuzzHost {
492 fn read_line(&mut self) -> Option<String> {
493 self.lines.next()
494 }
495 fn err(&mut self, _s: &str) {}
496 fn out(&mut self, _s: &str) {}
497 fn read_table(&self, path: &str) -> Option<String> {
498 let base = path.rsplit('/').next().unwrap_or(path);
499 self.tables.get(base).cloned()
500 }
501 fn run_date(&self, _tz: &str) -> Option<String> {
502 Some("Mon Jan 1 00:00:00 UTC 2024".to_string())
503 }
504 fn run_date_fmt(&self, _tz: &str, _fmt: &str) -> Option<String> {
505 Some("2024 01 01 00:00 Mon Jan".to_string())
506 }
507 fn zone_readable(&self, _path: &str) -> bool {
508 true
509 }
510 fn stdout_is_tty(&self) -> bool {
511 false
512 }
513 }
514
515 pub fn __fuzz_run(input: &str, iso3166: &str, zone1970: &str) {
517 let mut tables = std::collections::HashMap::new();
518 tables.insert("iso3166.tab".to_string(), iso3166.to_string());
519 tables.insert("zone1970.tab".to_string(), zone1970.to_string());
520 tables.insert("zonenow.tab".to_string(), zone1970.to_string());
521 let mut h = FuzzHost {
522 lines: input
523 .lines()
524 .map(|s| s.to_string())
525 .collect::<Vec<_>>()
526 .into_iter(),
527 tables,
528 };
529 let _ = run(&Options::default(), &mut h);
530 }
531
532 pub fn __fuzz_posix_tz(s: &str) {
534 let _ = posix_tz_valid(s);
535 }
536}
537
538#[cfg(kani)]
539mod kani_harness {
540 #[kani::proof]
545 fn menu_index_guard_is_sound() {
546 let len: usize = kani::any();
547 let n: usize = kani::any();
548 kani::assume(len <= 4096);
549 if (1..=len).contains(&n) {
550 assert!(n >= 1);
551 assert!(n - 1 < len);
552 }
553 }
554}