diff --git a/Cargo.lock b/Cargo.lock index 1a9269ddcb0..9589b975762 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,7 +77,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -88,7 +88,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1027,7 +1027,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1756,7 +1756,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2059,7 +2059,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2232,13 +2232,12 @@ dependencies = [ [[package]] name = "parse_datetime" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413775a7eac2261d2211a79d10ef275e5b6f7b527eec42ad09adce2ffa92b6e5" +version = "0.15.0" +source = "git+https://github.com/uutils/parse_datetime?tag=0.15.0#7440dd9ecb00b2bb50873b7803963bfcee4b2e8f" dependencies = [ "jiff", "num-traits", - "winnow", + "winnow 1.0.1", ] [[package]] @@ -2652,7 +2651,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2952,7 +2951,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2962,7 +2961,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -3088,7 +3087,7 @@ dependencies = [ "indexmap", "toml_datetime", "toml_parser", - "winnow", + "winnow 0.7.14", ] [[package]] @@ -3097,7 +3096,7 @@ version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" dependencies = [ - "winnow", + "winnow 0.7.14", ] [[package]] @@ -4741,7 +4740,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4974,6 +4973,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/Cargo.toml b/Cargo.toml index 33fdbe0d25e..955107b6d8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -417,7 +417,7 @@ num-bigint = "0.4.4" num-prime = "0.5.0" num-traits = "0.2.19" onig = { version = "~6.5.1", default-features = false } -parse_datetime = "0.14.0" +parse_datetime = { git = "https://github.com/uutils/parse_datetime", tag = "0.15.0" } phf = "0.13.1" phf_codegen = "0.13.1" platform-info = "2.0.3" diff --git a/deny.toml b/deny.toml index ea8396df467..c685c2659e4 100644 --- a/deny.toml +++ b/deny.toml @@ -114,6 +114,8 @@ skip = [ { name = "crypto-common", version = "0.1.7" }, # digest { name = "block-buffer", version = "0.10.4" }, + # parse_datetime, toml_edit + { name = "winnow", version = "1.0.1" }, ] # spell-checker: enable @@ -125,4 +127,4 @@ skip = [ unknown-registry = "warn" unknown-git = "warn" allow-registry = ["https://github.com/rust-lang/crates.io-index"] -allow-git = [] +allow-git = ["https://github.com/uutils/parse_datetime"] diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 8879c35b585..e5d1cc7913e 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -28,9 +28,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -43,15 +43,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -140,9 +140,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "blake2b_simd" @@ -157,16 +157,16 @@ dependencies = [ [[package]] name = "blake3" -version = "1.8.3" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +checksum = "4d2d5991425dfd0785aed03aedcf0b321d61975c9b5b3689c774a2610ae0b51e" dependencies = [ "arrayref", "arrayvec", "cc", "cfg-if", "constant_time_eq", - "cpufeatures 0.2.17", + "cpufeatures 0.3.0", ] [[package]] @@ -209,9 +209,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytecount" @@ -231,9 +231,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.55" +version = "1.2.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" dependencies = [ "find-msvc-tools", "jobserver", @@ -266,9 +266,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "num-traits", @@ -277,18 +277,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.57" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.57" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -299,15 +299,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.7" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "compare" @@ -324,7 +324,7 @@ dependencies = [ "encode_unicode", "libc", "unicode-width", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -392,13 +392,30 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc-fast" -version = "1.10.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e75b2483e97a5a7da73ac68a05b629f9c53cff58d8ed1c77866079e18b00dba5" +checksum = "2fd92aca2c6001b1bf5ba0ff84ee74ec8501b52bbef0cac80bf25a6c1d87a83d" dependencies = [ + "crc", "digest 0.10.7", + "rustversion", "spin", ] @@ -469,7 +486,7 @@ checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" dependencies = [ "dispatch2", "nix", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -521,9 +538,9 @@ dependencies = [ [[package]] name = "dispatch2" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ "bitflags", "block2", @@ -704,19 +721,19 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", ] [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "rand_core 0.10.0", "wasip2", "wasip3", @@ -763,9 +780,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hybrid-array" -version = "0.4.8" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8655f91cd07f2b9d0c24137bd650fe69617773435ee5ec83022377777ce65ef1" +checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" dependencies = [ "typenum", ] @@ -1109,15 +1126,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.19" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89a5b5e10d5a9ad6e5d1f4bd58225f655d6fe9767575a5e8ac5a6fe64e04495" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -1141,9 +1158,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.19" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff7a39c8862fc1369215ccf0a8f12dd4598c7f6484704359f0351bd617034dbf" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" dependencies = [ "proc-macro2", "quote", @@ -1152,9 +1169,9 @@ dependencies = [ [[package]] name = "jiff-tzdb" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68971ebff725b9e2ca27a601c5eb38a4c5d64422c4cbab0c535f248087eda5c2" +checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076" [[package]] name = "jiff-tzdb-platform" @@ -1177,9 +1194,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "797146bb2677299a1eb6b7b50a890f4c361b29ef967addf5b2fa45dae1bb6d7d" dependencies = [ "once_cell", "wasm-bindgen", @@ -1202,9 +1219,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.182" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libfuzzer-sys" @@ -1317,9 +1334,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", ] @@ -1332,9 +1349,9 @@ checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -1391,9 +1408,8 @@ checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" [[package]] name = "parse_datetime" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413775a7eac2261d2211a79d10ef275e5b6f7b527eec42ad09adce2ffa92b6e5" +version = "0.15.0" +source = "git+https://github.com/uutils/parse_datetime?tag=0.15.0#7440dd9ecb00b2bb50873b7803963bfcee4b2e8f" dependencies = [ "jiff", "num-traits", @@ -1414,9 +1430,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" dependencies = [ "portable-atomic", ] @@ -1486,9 +1502,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -1499,6 +1515,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.9.2" @@ -1516,7 +1538,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" dependencies = [ "chacha20", - "getrandom 0.4.1", + "getrandom 0.4.2", "rand_core 0.10.0", ] @@ -1590,9 +1612,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rust-ini" @@ -1606,9 +1628,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustix" @@ -1724,9 +1746,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "similar" @@ -1772,9 +1794,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.114" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -1799,7 +1821,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.1", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", @@ -1807,12 +1829,12 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.60.2", + "windows-sys", ] [[package]] @@ -1890,9 +1912,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-width" @@ -1974,7 +1996,7 @@ dependencies = [ "parse_datetime", "regex", "uucore", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -2158,7 +2180,7 @@ dependencies = [ "uucore_procs", "wild", "winapi-util", - "windows-sys 0.61.2", + "windows-sys", "z85", ] @@ -2244,9 +2266,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "7dc0882f7b5bb01ae8c5215a1230832694481c1a4be062fd410e12ea3da5b631" dependencies = [ "cfg-if", "once_cell", @@ -2257,9 +2279,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "75973d3066e01d035dbedaad2864c398df42f8dd7b1ea057c35b8407c015b537" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2267,9 +2289,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "91af5e4be765819e0bcfee7322c14374dc821e35e72fa663a830bbc7dc199eac" dependencies = [ "bumpalo", "proc-macro2", @@ -2280,9 +2302,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "c9bf0406a78f02f336bf1e451799cca198e8acde4ffa278f0fb20487b150a633" dependencies = [ "unicode-ident", ] @@ -2398,15 +2420,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -2416,76 +2429,11 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winnow" -version = "0.7.14" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" dependencies = [ "memchr", ] @@ -2618,24 +2566,24 @@ dependencies = [ [[package]] name = "z85" -version = "3.0.6" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b3a41ce106832b4da1c065baa4c31cf640cf965fa1483816402b7f6b96f0a64" +checksum = "c6e61e59a957b7ccee15d2049f86e8bfd6f66968fcd88f018950662d9b86e675" [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index e577d081c49..a94e72505eb 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore strtime ; (format) DATEFILE MMDDhhmm ; (vars) datetime datetimes getres AWST ACST AEST foobarbaz +// spell-checker:ignore strtime yday ; (format) DATEFILE MMDDhhmm ; (vars) datetime datetimes getres AWST ACST AEST foobarbaz mod format_modifiers; mod locale; @@ -79,6 +79,12 @@ impl DebugOptions { } } +enum ParsedDateValue { + InRange(Zoned), + Extended(parse_datetime::ExtendedDateTime), + ExtendedInZone(parse_datetime::ExtendedDateTime, TimeZone), +} + /// Various ways of displaying the date enum Format { Iso8601(Iso8601Format), @@ -364,18 +370,23 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { Zoned::now() }; - let set_to = match matches - .get_one::(OPT_SET) - .map(|s| parse_date(s, &now, DebugOptions::new(debug_mode, true))) - { + let set_to = match matches.get_one::(OPT_SET) { None => None, - Some(Err((input, _err))) => { - return Err(USimpleError::new( - 1, - translate!("date-error-invalid-date", "date" => input), - )); - } - Some(Ok(date)) => Some(date), + Some(s) => match parse_date(s, &now, DebugOptions::new(debug_mode, true)) { + Err((input, _err)) => { + return Err(USimpleError::new( + 1, + translate!("date-error-invalid-date", "date" => input), + )); + } + Ok(ParsedDateValue::InRange(date)) => Some(date), + Ok(ParsedDateValue::Extended(_) | ParsedDateValue::ExtendedInZone(_, _)) => { + return Err(USimpleError::new( + 1, + translate!("date-error-invalid-date", "date" => s.as_str()), + )); + } + }, }; let settings = Settings { @@ -525,18 +536,20 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { translate!("date-error-cannot-set-date", "path" => path.quote(), "error" => e), ) })?; - let date = ts.to_zoned(TimeZone::try_system().unwrap_or(TimeZone::UTC)); + let date = ParsedDateValue::InRange( + ts.to_zoned(TimeZone::try_system().unwrap_or(TimeZone::UTC)), + ); let iter = std::iter::once(Ok(date)); Box::new(iter) } DateSource::Resolution => { let resolution = get_clock_resolution(); - let date = resolution.to_zoned(TimeZone::system()); + let date = ParsedDateValue::InRange(resolution.to_zoned(TimeZone::system())); let iter = std::iter::once(Ok(date)); Box::new(iter) } DateSource::Now => { - let iter = std::iter::once(Ok(now)); + let iter = std::iter::once(Ok(ParsedDateValue::InRange(now.clone()))); Box::new(iter) } }; @@ -548,7 +561,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let config = Config::new().custom(PosixCustom::new()).lenient(true); for date in dates { match date { - Ok(date) => { + Ok(ParsedDateValue::InRange(date)) => { let date = if settings.utc { date.with_time_zone(TimeZone::UTC) } else { @@ -574,6 +587,66 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } } } + Ok(ParsedDateValue::Extended(date)) => { + let skip_localization = + matches!(settings.format, Format::Rfc5322 | Format::Rfc3339(_)); + let (date, tz_name, zone_display) = + convert_extended_for_output(&date, settings.utc, &now) + .map_err(|e| { + USimpleError::new( + 1, + translate!("date-error-invalid-format", "format" => format_string, "error" => e), + ) + })?; + match format_extended_date( + &date, + format_string, + &tz_name, + &zone_display, + skip_localization, + ) { + Ok(s) => writeln!(stdout, "{s}").map_err(|e| { + USimpleError::new(1, translate!("date-error-write", "error" => e)) + })?, + Err(e) => { + let _ = stdout.flush(); + return Err(USimpleError::new( + 1, + translate!("date-error-invalid-format", "format" => format_string, "error" => e), + )); + } + } + } + Ok(ParsedDateValue::ExtendedInZone(date, zone)) => { + let skip_localization = + matches!(settings.format, Format::Rfc5322 | Format::Rfc3339(_)); + let (date, tz_name, zone_display) = + preserve_extended_for_output_in_zone(&date, settings.utc, &zone) + .map_err(|e| { + USimpleError::new( + 1, + translate!("date-error-invalid-format", "format" => format_string, "error" => e), + ) + })?; + match format_extended_date( + &date, + format_string, + &tz_name, + &zone_display, + skip_localization, + ) { + Ok(s) => writeln!(stdout, "{s}").map_err(|e| { + USimpleError::new(1, translate!("date-error-write", "error" => e)) + })?, + Err(e) => { + let _ = stdout.flush(); + return Err(USimpleError::new( + 1, + translate!("date-error-invalid-format", "format" => format_string, "error" => e), + )); + } + } + } Err((input, _err)) => { let _ = stdout.flush(); show!(USimpleError::new( @@ -728,6 +801,625 @@ fn format_date_with_locale_aware_months( result.map_err(|e| e.to_string()) } +fn weekday_abbrev(weekday_sunday0: u8) -> &'static str { + const WEEKDAYS: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + WEEKDAYS[weekday_sunday0 as usize] +} + +fn weekday_full(weekday_sunday0: u8) -> &'static str { + const WEEKDAYS: [&str; 7] = [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + ]; + WEEKDAYS[weekday_sunday0 as usize] +} + +fn month_abbrev(month: u8) -> &'static str { + const MONTHS: [&str; 12] = [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", + ]; + MONTHS[(month - 1) as usize] +} + +fn month_full(month: u8) -> &'static str { + const MONTHS: [&str; 12] = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ]; + MONTHS[(month - 1) as usize] +} + +struct LocalizedNames { + weekday_abbrev: String, + weekday_full: String, + month_abbrev: String, + month_full: String, +} + +#[cfg(feature = "i18n-datetime")] +fn localized_names_for_extended( + date: &parse_datetime::ExtendedDateTime, + skip_localization: bool, +) -> Result, String> { + if skip_localization || !should_use_icu_locale() { + return Ok(None); + } + + let surrogate = jiff::civil::DateTime::new( + surrogate_year_for_rules(date.year), + date.month as i8, + date.day as i8, + date.hour as i8, + date.minute as i8, + date.second as i8, + date.nanosecond as i32, + ) + .map_err(|e| e.to_string())? + .to_zoned(TimeZone::UTC) + .map_err(|e| e.to_string())?; + + let config = Config::new().custom(PosixCustom::new()).lenient(true); + let format = |fmt: &str| -> Result { + let localized = localize_format_string(fmt, surrogate.date()); + BrokenDownTime::from(&surrogate) + .to_string_with_config(&config, &localized) + .map_err(|e| e.to_string()) + }; + + Ok(Some(LocalizedNames { + weekday_abbrev: format("%a")?, + weekday_full: format("%A")?, + month_abbrev: format("%b")?, + month_full: format("%B")?, + })) +} + +#[cfg(not(feature = "i18n-datetime"))] +fn localized_names_for_extended( + _date: &parse_datetime::ExtendedDateTime, + _skip_localization: bool, +) -> Result, String> { + Ok(None) +} + +fn format_offset(offset_seconds: i32, colon_count: usize) -> Result { + let sign = if offset_seconds < 0 { '-' } else { '+' }; + let abs = offset_seconds.unsigned_abs(); + let h = abs / 3600; + let m = (abs % 3600) / 60; + let s = abs % 60; + match colon_count { + 0 => Ok(format!("{sign}{h:02}{m:02}")), + 1 => Ok(format!("{sign}{h:02}:{m:02}")), + 2 => Ok(format!("{sign}{h:02}:{m:02}:{s:02}")), + 3 => { + if s != 0 { + Ok(format!("{sign}{h:02}:{m:02}:{s:02}")) + } else if m != 0 { + Ok(format!("{sign}{h:02}:{m:02}")) + } else { + Ok(format!("{sign}{h:02}")) + } + } + _ => Err(format!( + "unsupported extended format specifier %{}z", + ":".repeat(colon_count) + )), + } +} + +fn convert_extended_for_output( + date: &parse_datetime::ExtendedDateTime, + utc: bool, + now: &Zoned, +) -> Result<(parse_datetime::ExtendedDateTime, String, String), String> { + convert_extended_for_output_in_zone(date, utc, now.time_zone()) +} + +fn convert_extended_for_output_in_zone( + date: &parse_datetime::ExtendedDateTime, + utc: bool, + zone: &TimeZone, +) -> Result<(parse_datetime::ExtendedDateTime, String, String), String> { + let (target_offset, tz_name) = if utc { + (0, "UTC".to_string()) + } else { + offset_info_for_extended_in_zone(date, zone)? + }; + let zone_display = if utc { + "UTC".to_string() + } else if let Some(name) = zone.iana_name() { + name.to_string() + } else { + format_offset(target_offset, 0)? + }; + let converted = parse_datetime::ExtendedDateTime::from_unix_seconds( + date.unix_seconds(), + date.nanosecond, + target_offset, + ) + .map_err(str::to_string)?; + Ok((converted, tz_name, zone_display)) +} + +fn preserve_extended_for_output_in_zone( + date: &parse_datetime::ExtendedDateTime, + utc: bool, + zone: &TimeZone, +) -> Result<(parse_datetime::ExtendedDateTime, String, String), String> { + if utc { + return convert_extended_for_output_in_zone(date, true, zone); + } + + let (_, tz_name) = local_offset_info_for_extended_in_zone(date, zone)?; + let zone_display = if let Some(name) = zone.iana_name() { + name.to_string() + } else { + format_offset(date.offset_seconds, 0)? + }; + Ok((date.clone(), tz_name, zone_display)) +} + +fn resolve_extended_in_zone( + date: &parse_datetime::ExtendedDateTime, + zone: &TimeZone, +) -> Result { + let (surrogate_local, zoned) = surrogate_zoned_for_extended(date, zone)?; + let _ = surrogate_local; + parse_datetime::ExtendedDateTime::new( + parse_datetime::DateParts { + year: date.year, + month: date.month, + day: date.day, + }, + parse_datetime::TimeParts { + hour: date.hour, + minute: date.minute, + second: date.second, + nanosecond: date.nanosecond, + }, + zoned.offset().seconds(), + ) + .map_err(str::to_string) +} + +fn surrogate_zoned_for_extended( + date: &parse_datetime::ExtendedDateTime, + zone: &TimeZone, +) -> Result<(jiff::civil::DateTime, Zoned), String> { + let surrogate_local = jiff::civil::DateTime::new( + surrogate_year_for_rules(date.year), + date.month as i8, + date.day as i8, + date.hour as i8, + date.minute as i8, + date.second as i8, + date.nanosecond as i32, + ) + .map_err(|e| e.to_string())?; + let zoned = zone + .to_ambiguous_zoned(surrogate_local) + .compatible() + .map_err(|e| e.to_string())?; + Ok((surrogate_local, zoned)) +} + +fn surrogate_year_for_rules(year: u32) -> i16 { + if year <= 9_999 { + year as i16 + } else { + const BASE: u32 = 9_600; + (BASE + ((year - BASE) % 400)) as i16 + } +} + +fn offset_info_for_extended_in_zone( + date: &parse_datetime::ExtendedDateTime, + zone: &TimeZone, +) -> Result<(i32, String), String> { + let utc = parse_datetime::ExtendedDateTime::from_unix_seconds( + date.unix_seconds(), + date.nanosecond, + 0, + ) + .map_err(str::to_string)?; + let surrogate = jiff::civil::DateTime::new( + surrogate_year_for_rules(utc.year), + utc.month as i8, + utc.day as i8, + utc.hour as i8, + utc.minute as i8, + utc.second as i8, + utc.nanosecond as i32, + ) + .map_err(|e| e.to_string())?; + let surrogate_ts = surrogate + .to_zoned(TimeZone::UTC) + .map_err(|e| e.to_string())? + .timestamp(); + let info = zone.to_offset_info(surrogate_ts); + Ok((info.offset().seconds(), info.abbreviation().to_string())) +} + +fn local_offset_info_for_extended_in_zone( + date: &parse_datetime::ExtendedDateTime, + zone: &TimeZone, +) -> Result<(i32, String), String> { + let (_, zoned) = surrogate_zoned_for_extended(date, zone)?; + let info = zone.to_offset_info(zoned.timestamp()); + Ok((info.offset().seconds(), info.abbreviation().to_string())) +} + +#[derive(Clone, Copy)] +struct WeekFields { + sunday_week: u8, + monday_week: u8, + iso_week: u8, + iso_year: u32, +} + +fn compute_week_fields(date: &parse_datetime::ExtendedDateTime) -> Result { + let jan1 = parse_datetime::ExtendedDateTime::new( + parse_datetime::DateParts { + year: date.year, + month: 1, + day: 1, + }, + parse_datetime::TimeParts { + hour: 0, + minute: 0, + second: 0, + nanosecond: 0, + }, + 0, + ) + .map_err(str::to_string)?; + let yday0 = i32::from(date.day_of_year()) - 1; + let jan1_sunday0 = i32::from(jan1.weekday_sunday0()); + let jan1_monday0 = (jan1_sunday0 + 6).rem_euclid(7); + + let first_sunday = (7 - jan1_sunday0).rem_euclid(7); + let sunday_week = if yday0 < first_sunday { + 0 + } else { + 1 + (yday0 - first_sunday) / 7 + }; + let first_monday = (7 - jan1_monday0).rem_euclid(7); + let monday_week = if yday0 < first_monday { + 0 + } else { + 1 + (yday0 - first_monday) / 7 + }; + + let surrogate_year = surrogate_year_for_rules(date.year); + let surrogate_date = jiff::civil::Date::new(surrogate_year, date.month as i8, date.day as i8) + .map_err(|e| e.to_string())?; + let iso = surrogate_date.iso_week_date(); + let iso_year_delta = i64::from(date.year) - i64::from(surrogate_year); + let iso_year = i64::from(iso.year()) + iso_year_delta; + if iso_year < 0 || iso_year > i64::from(u32::MAX) { + return Err("ISO week year is out of range".to_string()); + } + + Ok(WeekFields { + sunday_week: sunday_week as u8, + monday_week: monday_week as u8, + iso_week: iso.week() as u8, + iso_year: iso_year as u32, + }) +} + +#[derive(Default)] +struct FormatFlags { + no_pad: bool, + pad: Option, + upper: bool, +} + +fn apply_case(s: &str, flags: &FormatFlags) -> String { + if flags.upper { + s.to_ascii_uppercase() + } else { + s.to_string() + } +} + +fn format_num( + value: u64, + default_width: usize, + default_pad: char, + width: Option, + flags: &FormatFlags, +) -> String { + if flags.no_pad { + return value.to_string(); + } + let width = width.unwrap_or(default_width); + let pad = flags.pad.unwrap_or(default_pad); + if pad == '0' { + format!("{value:0width$}") + } else { + format!("{value:>width$}") + } +} + +fn format_extended_date( + date: &parse_datetime::ExtendedDateTime, + format_string: &str, + tz_name: &str, + zone_display: &str, + skip_localization: bool, +) -> Result { + let mut out = String::new(); + let mut chars = format_string.chars().peekable(); + let mut week_fields: Option = None; + let localized_names = localized_names_for_extended(date, skip_localization)?; + + while let Some(ch) = chars.next() { + if ch != '%' { + out.push(ch); + continue; + } + + if let Some(':') = chars.peek().copied() { + let mut colons = 0usize; + while let Some(':') = chars.peek().copied() { + chars.next(); + colons += 1; + } + match chars.next() { + Some('z') => out.push_str(&format_offset(date.offset_seconds, colons)?), + Some(other) => { + let specifier = ":".repeat(colons); + return Err(format!( + "unsupported extended format specifier %{specifier}{other}" + )); + } + None => return Err("trailing '%' in format string".to_string()), + } + continue; + } + + let mut raw_token = String::from("%"); + let mut flags = FormatFlags::default(); + loop { + match chars.peek().copied() { + Some('-') => { + flags.no_pad = true; + raw_token.push('-'); + chars.next(); + } + Some('_') => { + flags.pad = Some(' '); + raw_token.push('_'); + chars.next(); + } + Some('0') => { + flags.pad = Some('0'); + raw_token.push('0'); + chars.next(); + } + Some('^') => { + flags.upper = true; + raw_token.push('^'); + chars.next(); + } + Some('#') => { + raw_token.push('#'); + chars.next(); + } + _ => break, + } + } + + let mut width: Option = None; + while let Some(c) = chars.peek().copied() { + if c.is_ascii_digit() { + chars.next(); + raw_token.push(c); + let digit = (c as u8 - b'0') as usize; + width = Some(width.unwrap_or(0).saturating_mul(10).saturating_add(digit)); + } else { + break; + } + } + + let modifier = match chars.peek().copied() { + Some('E' | 'O') => { + let modifier = chars.next().expect("peeked modifier must exist"); + raw_token.push(modifier); + Some(modifier) + } + _ => None, + }; + + let spec = chars + .next() + .ok_or_else(|| "trailing '%' in format string".to_string())?; + raw_token.push(spec); + if modifier.is_some() { + out.push_str(&raw_token); + continue; + } + let ampm = if date.hour < 12 { "AM" } else { "PM" }; + let hour12 = match date.hour % 12 { + 0 => 12, + h => h, + }; + let y2 = date.year % 100; + let century = date.year / 100; + + let piece = match spec { + '%' => "%".to_string(), + 'Y' => format_num(date.year as u64, 4, '0', width, &flags), + 'C' => format_num(century as u64, 2, '0', width, &flags), + 'y' => format_num(y2 as u64, 2, '0', width, &flags), + 'm' => format_num(date.month as u64, 2, '0', width, &flags), + 'd' => format_num(date.day as u64, 2, '0', width, &flags), + 'e' => format_num(date.day as u64, 2, ' ', width, &flags), + 'j' => format_num(date.day_of_year() as u64, 3, '0', width, &flags), + 'H' => format_num(date.hour as u64, 2, '0', width, &flags), + 'I' => format_num(hour12 as u64, 2, '0', width, &flags), + 'k' => format_num(date.hour as u64, 2, ' ', width, &flags), + 'l' => format_num(hour12 as u64, 2, ' ', width, &flags), + 'M' => format_num(date.minute as u64, 2, '0', width, &flags), + 'S' => format_num(date.second as u64, 2, '0', width, &flags), + 'N' => format_num(date.nanosecond as u64, 9, '0', width, &flags), + 'q' => format_num(((date.month - 1) / 3 + 1) as u64, 1, '0', width, &flags), + 'Q' => apply_case(zone_display, &flags), + 'p' => apply_case(ampm, &flags), + 'P' => apply_case(&m.to_ascii_lowercase(), &flags), + 'a' => apply_case( + localized_names.as_ref().map_or_else( + || weekday_abbrev(date.weekday_sunday0()), + |names| names.weekday_abbrev.as_str(), + ), + &flags, + ), + 'A' => apply_case( + localized_names.as_ref().map_or_else( + || weekday_full(date.weekday_sunday0()), + |names| names.weekday_full.as_str(), + ), + &flags, + ), + 'b' | 'h' => apply_case( + localized_names.as_ref().map_or_else( + || month_abbrev(date.month), + |names| names.month_abbrev.as_str(), + ), + &flags, + ), + 'B' => apply_case( + localized_names + .as_ref() + .map_or_else(|| month_full(date.month), |names| names.month_full.as_str()), + &flags, + ), + 'u' => { + let weekday_monday1 = match date.weekday_sunday0() { + 0 => 7, + w => w, + }; + format_num(weekday_monday1 as u64, 1, '0', width, &flags) + } + 'w' => format_num(date.weekday_sunday0() as u64, 1, '0', width, &flags), + 'U' => { + if week_fields.is_none() { + week_fields = Some(compute_week_fields(date)?); + } + let wf = week_fields + .as_ref() + .ok_or_else(|| "failed to compute week fields".to_string())?; + format_num(wf.sunday_week as u64, 2, '0', width, &flags) + } + 'W' => { + if week_fields.is_none() { + week_fields = Some(compute_week_fields(date)?); + } + let wf = week_fields + .as_ref() + .ok_or_else(|| "failed to compute week fields".to_string())?; + format_num(wf.monday_week as u64, 2, '0', width, &flags) + } + 'V' => { + if week_fields.is_none() { + week_fields = Some(compute_week_fields(date)?); + } + let wf = week_fields + .as_ref() + .ok_or_else(|| "failed to compute week fields".to_string())?; + format_num(wf.iso_week as u64, 2, '0', width, &flags) + } + 'G' => { + if week_fields.is_none() { + week_fields = Some(compute_week_fields(date)?); + } + let wf = week_fields + .as_ref() + .ok_or_else(|| "failed to compute week fields".to_string())?; + format_num(wf.iso_year as u64, 4, '0', width, &flags) + } + 'g' => { + if week_fields.is_none() { + week_fields = Some(compute_week_fields(date)?); + } + let wf = week_fields + .as_ref() + .ok_or_else(|| "failed to compute week fields".to_string())?; + format_num((wf.iso_year % 100) as u64, 2, '0', width, &flags) + } + 'F' => format!( + "{}-{}-{}", + format_num(date.year as u64, 4, '0', None, &FormatFlags::default()), + format_num(date.month as u64, 2, '0', None, &FormatFlags::default()), + format_num(date.day as u64, 2, '0', None, &FormatFlags::default()) + ), + 'D' | 'x' => format!( + "{}/{}/{}", + format_num(date.month as u64, 2, '0', None, &FormatFlags::default()), + format_num(date.day as u64, 2, '0', None, &FormatFlags::default()), + format_num(y2 as u64, 2, '0', None, &FormatFlags::default()) + ), + 'T' | 'X' => format!( + "{}:{}:{}", + format_num(date.hour as u64, 2, '0', None, &FormatFlags::default()), + format_num(date.minute as u64, 2, '0', None, &FormatFlags::default()), + format_num(date.second as u64, 2, '0', None, &FormatFlags::default()) + ), + 'R' => format!( + "{}:{}", + format_num(date.hour as u64, 2, '0', None, &FormatFlags::default()), + format_num(date.minute as u64, 2, '0', None, &FormatFlags::default()) + ), + 'r' => { + let hour = format_num(hour12 as u64, 2, '0', None, &FormatFlags::default()); + let minute = format_num(date.minute as u64, 2, '0', None, &FormatFlags::default()); + let second = format_num(date.second as u64, 2, '0', None, &FormatFlags::default()); + format!("{hour}:{minute}:{second} {ampm}") + } + 'c' => { + let weekday = weekday_abbrev(date.weekday_sunday0()); + let month = month_abbrev(date.month); + let day = format_num(date.day as u64, 2, ' ', None, &FormatFlags::default()); + let hour = format_num(date.hour as u64, 2, '0', None, &FormatFlags::default()); + let minute = format_num(date.minute as u64, 2, '0', None, &FormatFlags::default()); + let second = format_num(date.second as u64, 2, '0', None, &FormatFlags::default()); + let year = format_num(date.year as u64, 4, '0', None, &FormatFlags::default()); + format!("{weekday} {month} {day} {hour}:{minute}:{second} {year}") + } + 'Z' => apply_case(tz_name, &flags), + 'z' => format_offset(date.offset_seconds, 0)?, + 's' => date.unix_seconds().to_string(), + 'n' => "\n".to_string(), + 't' => "\t".to_string(), + other => { + return Err(format!( + "unsupported format specifier %{other} for out-of-range year" + )); + } + }; + out.push_str(&piece); + } + + Ok(out) +} + /// Return the appropriate format string for the given settings. fn make_format_string(settings: &Settings) -> &str { match settings.format { @@ -824,10 +1516,10 @@ fn tz_abbrev_to_iana(abbrev: &str) -> Option<&str> { /// Attempts to parse a date string that contains a timezone abbreviation (e.g. "EST"). /// -/// If an abbreviation is found and the date is parsable, returns `Some(Zoned)`. +/// If an abbreviation is found and the date is parsable, returns `Some(ParsedDateValue)`. /// Returns `None` if no abbreviation is detected or if parsing fails, indicating /// that standard parsing should be attempted. -fn try_parse_with_abbreviation>(date_str: S, now: &Zoned) -> Option { +fn try_parse_with_abbreviation>(date_str: S, now: &Zoned) -> Option { let s = date_str.as_ref(); // Look for timezone abbreviation at the end of the string @@ -851,9 +1543,18 @@ fn try_parse_with_abbreviation>(date_str: S, now: &Zoned) -> Optio let date_part = s.trim_end_matches(last_word).trim(); // Parse in the target timezone so "10:30 EDT" means 10:30 in EDT if let Ok(parsed) = parse_datetime::parse_datetime_at_date(now.clone(), date_part) { - let dt = parsed.datetime(); - if let Ok(zoned) = dt.to_zoned(tz) { - return Some(zoned); + match parsed { + parse_datetime::ParsedDateTime::InRange(parsed) => { + let dt = parsed.datetime(); + if let Ok(zoned) = dt.to_zoned(tz) { + return Some(ParsedDateValue::InRange(zoned)); + } + } + parse_datetime::ParsedDateTime::Extended(parsed) => { + if let Ok(date) = resolve_extended_in_zone(&parsed, &tz) { + return Some(ParsedDateValue::ExtendedInZone(date, tz)); + } + } } } } @@ -864,9 +1565,7 @@ fn try_parse_with_abbreviation>(date_str: S, now: &Zoned) -> Optio None } -/// Parse a `String` into a `DateTime`. -/// If it fails, return a tuple of the `String` along with its `ParseError`. -/// Helper function to parse dates from a line-based reader (stdin or file) +/// Helper function to parse dates from a line-based reader (stdin or file). /// /// Takes any `Read` source, reads it line by line, and parses each line as a date. /// Returns a boxed iterator over the parse results. @@ -874,7 +1573,9 @@ fn parse_dates_from_reader( reader: R, now: &Zoned, dbg_opts: DebugOptions, -) -> Box> + '_> { +) -> Box< + dyn Iterator> + '_, +> { let lines = BufReader::new(reader).lines(); Box::new( lines @@ -883,18 +1584,15 @@ fn parse_dates_from_reader( ) } +/// Parse a string into a date value. /// -/// **Update for parse_datetime 0.13:** -/// - parse_datetime 0.11: returned `chrono::DateTime` → required conversion to `jiff::Zoned` -/// - parse_datetime 0.13: returns `jiff::Zoned` directly → no conversion needed -/// -/// This change was necessary to fix issue #8754 (parsing large second values like -/// "12345.123456789 seconds ago" which failed in 0.11 but works in 0.13). +/// `parse_datetime` returns `ParsedDateTime` directly so we can preserve +/// large-year results alongside normal `Zoned` values. fn parse_date + Clone>( s: S, now: &Zoned, dbg_opts: DebugOptions, -) -> Result { +) -> Result { let input_str = s.as_ref(); if dbg_opts.debug { @@ -902,32 +1600,56 @@ fn parse_date + Clone>( } // First, try to parse any timezone abbreviations - if let Some(zoned) = try_parse_with_abbreviation(input_str, now) { + if let Some(parsed) = try_parse_with_abbreviation(input_str, now) { if dbg_opts.debug { let mut err = stderr().lock(); - let _ = writeln!( - err, - "date: parsed date part: (Y-M-D) {}", - strtime::format("%Y-%m-%d", &zoned).unwrap_or_default() - ); - let _ = writeln!( - err, - "date: parsed time part: {}", - strtime::format("%H:%M:%S", &zoned).unwrap_or_default() - ); - let tz_display = zoned.time_zone().iana_name().unwrap_or("system default"); - let _ = writeln!(err, "date: input timezone: {tz_display}"); + match &parsed { + ParsedDateValue::InRange(zoned) => { + let _ = writeln!( + err, + "date: parsed date part: (Y-M-D) {}", + strtime::format("%Y-%m-%d", zoned).unwrap_or_default() + ); + let _ = writeln!( + err, + "date: parsed time part: {}", + strtime::format("%H:%M:%S", zoned).unwrap_or_default() + ); + } + ParsedDateValue::Extended(date) => { + let _ = writeln!( + err, + "date: parsed date part: (Y-M-D) {:04}-{:02}-{:02}", + date.year, date.month, date.day + ); + let _ = writeln!( + err, + "date: parsed time part: {:02}:{:02}:{:02}", + date.hour, date.minute, date.second + ); + } + ParsedDateValue::ExtendedInZone(date, _) => { + let _ = writeln!( + err, + "date: parsed date part: (Y-M-D) {:04}-{:02}-{:02}", + date.year, date.month, date.day + ); + let _ = writeln!( + err, + "date: parsed time part: {:02}:{:02}:{:02}", + date.hour, date.minute, date.second + ); + } + } + let _ = writeln!(err, "date: input timezone: timezone abbreviation"); } - return Ok(zoned); + return Ok(parsed); } match parse_datetime::parse_datetime_at_date(now.clone(), input_str) { - // Convert to system timezone for display - // (parse_datetime 0.13 returns Zoned in the input's timezone) - Ok(date) => { + Ok(parse_datetime::ParsedDateTime::InRange(date)) => { let result = date.timestamp().to_zoned(now.time_zone().clone()); if dbg_opts.debug { - // Show final parsed date and time let mut err = stderr().lock(); let _ = writeln!( err, @@ -940,14 +1662,9 @@ fn parse_date + Clone>( strtime::format("%H:%M:%S", &result).unwrap_or_default() ); - // Show timezone information let _ = writeln!(err, "date: input timezone: system default"); - // Check if time component was specified, if not warn about midnight usage - // Only warn for date-only inputs (no time specified), but not for epoch formats (@N) - // or inputs that explicitly specify a time (containing ':') if dbg_opts.warn_midnight && !input_str.contains(':') && !input_str.contains('@') { - // Input likely didn't specify a time, so midnight was assumed let time_str = strtime::format("%H:%M:%S", &result).unwrap_or_default(); if time_str == "00:00:00" { let _ = writeln!( @@ -957,7 +1674,37 @@ fn parse_date + Clone>( } } } - Ok(result) + Ok(ParsedDateValue::InRange(result)) + } + Ok(parse_datetime::ParsedDateTime::Extended(date)) => { + if dbg_opts.debug { + let mut err = stderr().lock(); + let _ = writeln!( + err, + "date: parsed date part: (Y-M-D) {:04}-{:02}-{:02}", + date.year, date.month, date.day + ); + let _ = writeln!( + err, + "date: parsed time part: {:02}:{:02}:{:02}", + date.hour, date.minute, date.second + ); + let _ = writeln!(err, "date: input timezone: system default"); + + if dbg_opts.warn_midnight + && !input_str.contains(':') + && !input_str.contains('@') + && date.hour == 0 + && date.minute == 0 + && date.second == 0 + { + let _ = writeln!( + err, + "date: warning: using midnight as starting time: 00:00:00" + ); + } + } + Ok(ParsedDateValue::Extended(date)) } Err(e) => Err((input_str.into(), e)), } @@ -1117,7 +1864,14 @@ mod tests { let now = "2025-03-15T20:00:00+00:00[UTC]".parse::().unwrap(); let result = parse_date("yesterday 10:00 GMT", &now, DebugOptions::new(false, false)).unwrap(); - assert_eq!(result.date(), jiff::civil::date(2025, 3, 14)); + match result { + ParsedDateValue::InRange(zoned) => { + assert_eq!(zoned.date(), jiff::civil::date(2025, 3, 14)); + } + ParsedDateValue::Extended(_) | ParsedDateValue::ExtendedInZone(_, _) => { + panic!("expected in-range date") + } + } } #[test] @@ -1130,6 +1884,12 @@ mod tests { DebugOptions::new(false, false), ) .unwrap(); + let date = match date { + ParsedDateValue::InRange(zoned) => zoned, + ParsedDateValue::Extended(_) | ParsedDateValue::ExtendedInZone(_, _) => { + panic!("expected in-range date") + } + }; let utc = convert_for_set(date, true); assert_eq!((utc.hour(), utc.minute(), utc.second()), (6, 53, 1)); // AWST(+08:00) -> -8h } diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index d1080c1dddf..2af192ab644 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -623,24 +623,35 @@ fn try_futimens_via_write_fd(path: &Path, atime: FileTime, mtime: FileTime) -> s let mtime_nsec = i64::from(mtime.nanoseconds()); #[cfg(target_pointer_width = "32")] - let atime_spec = TimeSpec::new( - atime_sec.try_into().unwrap(), - atime_nsec.try_into().unwrap(), - ); + let atime_spec = timespec_from_parts(atime_sec, atime_nsec)?; #[cfg(target_pointer_width = "64")] let atime_spec = TimeSpec::new(atime_sec, atime_nsec); #[cfg(target_pointer_width = "32")] - let mtime_spec = TimeSpec::new( - mtime_sec.try_into().unwrap(), - mtime_nsec.try_into().unwrap(), - ); + let mtime_spec = timespec_from_parts(mtime_sec, mtime_nsec)?; #[cfg(target_pointer_width = "64")] let mtime_spec = TimeSpec::new(mtime_sec, mtime_nsec); futimens(&file, &atime_spec, &mtime_spec).map_err(Error::from) } +#[cfg(all(unix, target_pointer_width = "32"))] +fn timespec_from_parts(seconds: i64, nanoseconds: i64) -> std::io::Result { + let seconds = i32::try_from(seconds).map_err(|_| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "timestamp is out of range for futimens on 32-bit targets", + ) + })?; + let nanoseconds = i32::try_from(nanoseconds).map_err(|_| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "timestamp nanoseconds are out of range for futimens on 32-bit targets", + ) + })?; + Ok(TimeSpec::new(seconds, nanoseconds)) +} + /// Get metadata of the provided path /// If `follow` is `true`, the function will try to follow symlinks. Errors if the symlink is dangling, otherwise defaults to symlink metadata. /// If `follow` is `false`, the function will return metadata of the symlink itself @@ -724,8 +735,15 @@ fn parse_date(ref_zoned: Zoned, s: &str) -> Result { } } - if let Ok(zoned) = parse_datetime::parse_datetime_at_date(ref_zoned, s) { - return Ok(timestamp_to_filetime(zoned.timestamp())); + if let Ok(parsed) = parse_datetime::parse_datetime_at_date(ref_zoned, s) { + return Ok(match parsed { + parse_datetime::ParsedDateTime::InRange(zoned) => { + timestamp_to_filetime(zoned.timestamp()) + } + parse_datetime::ParsedDateTime::Extended(dt) => { + FileTime::from_unix_time(dt.unix_seconds(), dt.nanosecond) + } + }); } Err(TouchError::InvalidDateFormat(s.to_owned())) diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index a6139848c08..390d8fbbb07 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -65,6 +65,233 @@ fn test_invalid_short_option() { .stderr_contains("unexpected argument '-w'"); } +#[test] +fn test_large_year_absolute_date() { + new_ucmd!() + .env("LANG", "C") + .env("TZ", "UTC0") + .arg("-d") + .arg("18978-01-01") + .succeeds() + .stdout_is("Thu Jan 1 00:00:00 UTC 18978\n"); +} + +#[test] +fn test_large_year_epoch_format() { + new_ucmd!() + .env("LANG", "C") + .env("TZ", "UTC0") + .arg("-d") + .arg("10000-01-01") + .arg("+%s") + .succeeds() + .stdout_is("253402300800\n"); +} + +#[test] +fn test_large_year_upper_bound() { + new_ucmd!() + .env("LANG", "C") + .env("TZ", "UTC0") + .arg("-d") + .arg("2147485547-12-31") + .arg("+%F") + .succeeds() + .stdout_is("2147485547-12-31\n"); + + new_ucmd!() + .env("LANG", "C") + .env("TZ", "UTC0") + .arg("-d") + .arg("2147485548-01-01") + .fails() + .stderr_contains("invalid date"); +} + +#[test] +fn test_large_year_with_explicit_offset_converts_to_output_timezone() { + new_ucmd!() + .env("LANG", "C") + .env("TZ", "UTC0") + .arg("-d") + .arg("10000-12-31 12:34:56 -0530") + .arg("+%Y-%m-%d %H:%M:%S %:z") + .succeeds() + .stdout_is("10000-12-31 18:04:56 +00:00\n"); +} + +#[test] +fn test_large_year_supports_r_and_c_formats() { + new_ucmd!() + .env("LANG", "C") + .env("TZ", "UTC0") + .arg("-d") + .arg("10000-12-31 12:34:56 -0530") + .arg("+%r") + .succeeds() + .stdout_is("06:04:56 PM\n"); + + new_ucmd!() + .env("LANG", "C") + .env("TZ", "UTC0") + .arg("-d") + .arg("10000-01-01") + .arg("+%c") + .succeeds() + .stdout_is("Sat Jan 1 00:00:00 10000\n"); +} + +#[test] +fn test_large_year_iana_timezone_rule_uses_dst_in_far_future() { + new_ucmd!() + .env("LANG", "C") + .env("TZ", "UTC0") + .arg("-d") + .arg(r#"TZ="America/New_York" 10000-07-01 00:00"#) + .arg("+%Y-%m-%d %H:%M:%S %:z %Z") + .succeeds() + .stdout_is("10000-07-01 04:00:00 +00:00 UTC\n"); + + new_ucmd!() + .env("LANG", "C") + .env("TZ", "America/New_York") + .arg("-d") + .arg("10000-07-01 00:00") + .arg("+%Y-%m-%d %H:%M:%S %:z %Z") + .succeeds() + .stdout_is("10000-07-01 00:00:00 -04:00 EDT\n"); +} + +#[test] +fn test_large_year_week_format_specifiers() { + new_ucmd!() + .env("LANG", "C") + .env("TZ", "UTC0") + .arg("-d") + .arg("10000-01-01") + .arg("+%U %W %V %G %g") + .succeeds() + .stdout_is("00 00 52 9999 99\n"); +} + +#[test] +fn test_large_year_supports_existing_q_format_in_local_timezone() { + new_ucmd!() + .env("LANG", "C") + .env("TZ", "America/New_York") + .arg("-d") + .arg("10000-05-01") + .arg("+%Q") + .succeeds() + .stdout_is("America/New_York\n"); +} + +#[test] +fn test_large_year_supports_existing_q_format_in_utc_mode() { + new_ucmd!() + .env("LANG", "C") + .env("TZ", "America/New_York") + .arg("-u") + .arg("-d") + .arg("10000-05-01") + .arg("+%Q") + .succeeds() + .stdout_is("UTC\n"); +} + +#[test] +fn test_large_year_supports_existing_q_format_for_fixed_offset_output() { + new_ucmd!() + .env("LANG", "C") + .env("TZ", "UTC0") + .arg("-d") + .arg("10000-05-01") + .arg("+%Q") + .succeeds() + .stdout_is("+0000\n"); +} + +#[test] +fn test_large_year_supports_existing_quarter_format() { + new_ucmd!() + .env("LANG", "C") + .env("TZ", "UTC0") + .arg("-d") + .arg("10000-05-01") + .arg("+%q") + .succeeds() + .stdout_is("2\n"); +} + +#[test] +fn test_large_year_preserves_existing_eo_literal_behavior() { + new_ucmd!() + .env("LANG", "C") + .env("TZ", "UTC0") + .arg("-d") + .arg("10000-01-01") + .arg("+%OB|%EA|%Od") + .succeeds() + .stdout_is("%OB|%EA|%Od\n"); +} + +#[test] +fn test_large_year_matches_in_range_existing_q_and_modifier_behavior() { + let in_range_q = new_ucmd!() + .env("LANG", "C") + .env("TZ", "UTC0") + .arg("-d") + .arg("2024-01-01") + .arg("+%Q") + .succeeds() + .stdout_str() + .to_string(); + let large_year_q = new_ucmd!() + .env("LANG", "C") + .env("TZ", "UTC0") + .arg("-d") + .arg("10000-01-01") + .arg("+%Q") + .succeeds() + .stdout_str() + .to_string(); + assert_eq!(large_year_q, in_range_q); + + let in_range_modifiers = new_ucmd!() + .env("LANG", "C") + .env("TZ", "UTC0") + .arg("-d") + .arg("2024-01-01") + .arg("+%OB|%EA|%Od") + .succeeds() + .stdout_str() + .to_string(); + let large_year_modifiers = new_ucmd!() + .env("LANG", "C") + .env("TZ", "UTC0") + .arg("-d") + .arg("10000-01-01") + .arg("+%OB|%EA|%Od") + .succeeds() + .stdout_str() + .to_string(); + assert_eq!(large_year_modifiers, in_range_modifiers); +} + +#[test] +#[cfg(unix)] +fn test_large_year_respects_localized_month_names() { + let scene = TestScenario::new(util_name!()); + + scene + .ucmd() + .env("LC_ALL", "fr_FR.UTF-8") + .env("TZ", "UTC0") + .args(&["-d", "10000-01-01", "+%B"]) + .succeeds() + .stdout_is("janvier\n"); +} + #[test] fn test_format_option_not_to_capture_other_valid_arguments() { new_ucmd!() @@ -2799,3 +3026,96 @@ fn test_korean_time_zone() { .succeeds() .stdout_is("2026-01-15 01:00:00 UTC\n"); } + +#[test] +fn test_large_year_timezone_abbreviations() { + new_ucmd!() + .env("TZ", "UTC0") + .arg("-d") + .arg("10000-01-01 00:00 KST") + .arg("+%Q|%:z|%Z") + .succeeds() + .stdout_is("+0900|+09:00|+09\n"); + + new_ucmd!() + .env("TZ", "UTC0") + .arg("-d") + .arg("10000-01-01 00:00 MEZ") + .arg("+%Q|%:z|%Z") + .succeeds() + .stdout_is("+0100|+01:00|+01\n"); + + new_ucmd!() + .env("TZ", "UTC0") + .arg("-d") + .arg("10000-01-01 00:00 SAMOA") + .arg("+%Q|%:z|%Z") + .succeeds() + .stdout_is("Pacific/Samoa|-11:00|SST\n"); + + new_ucmd!() + .env("TZ", "UTC") + .arg("-u") + .arg("-d") + .arg("10000-01-01 00:00 KST") + .arg("+%F %T %:z %Z") + .succeeds() + .stdout_is("9999-12-31 15:00:00 +00:00 UTC\n"); + + new_ucmd!() + .env("TZ", "UTC") + .arg("-u") + .arg("-d") + .arg("10000-01-01 00:00 MEZ") + .arg("+%F %T %:z %Z") + .succeeds() + .stdout_is("9999-12-31 23:00:00 +00:00 UTC\n"); + + new_ucmd!() + .env("TZ", "UTC") + .arg("-u") + .arg("-d") + .arg("10000-07-01 00:00 EDT") + .arg("+%F %T %:z %Z") + .succeeds() + .stdout_is("10000-07-01 04:00:00 +00:00 UTC\n"); +} + +#[test] +fn test_large_year_timezone_abbreviations_match_in_range_behavior() { + let in_range_kst = new_ucmd!() + .env("TZ", "UTC0") + .arg("-d") + .arg("2024-01-01 00:00 KST") + .arg("+%Q|%:z|%Z") + .succeeds() + .stdout_str() + .to_string(); + let large_year_kst = new_ucmd!() + .env("TZ", "UTC0") + .arg("-d") + .arg("10000-01-01 00:00 KST") + .arg("+%Q|%:z|%Z") + .succeeds() + .stdout_str() + .to_string(); + assert_eq!(large_year_kst, in_range_kst); + + let in_range_samoa = new_ucmd!() + .env("TZ", "UTC0") + .arg("-d") + .arg("2024-01-01 00:00 SAMOA") + .arg("+%Q|%:z|%Z") + .succeeds() + .stdout_str() + .to_string(); + let large_year_samoa = new_ucmd!() + .env("TZ", "UTC0") + .arg("-d") + .arg("10000-01-01 00:00 SAMOA") + .arg("+%Q|%:z|%Z") + .succeeds() + .stdout_str() + .to_string(); + assert_eq!(large_year_samoa, in_range_samoa); +} diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs index 29425d6aa6c..88f062ee591 100644 --- a/tests/by-util/test_touch.rs +++ b/tests/by-util/test_touch.rs @@ -58,6 +58,28 @@ fn test_touch_default() { assert!(at.file_exists(file)); } +#[test] +fn test_touch_large_year_date_does_not_panic() { + let (at, mut ucmd) = at_and_ucmd!(); + let file = "test_touch_large_year_date_does_not_panic"; + let result = ucmd.args(&["-d", "10000-01-01 00:00:00", file]).run(); + + let stderr = String::from_utf8_lossy(result.stderr()); + let code = result.try_exit_status().and_then(|s| s.code()); + assert_ne!( + code, + Some(101), + "touch must not panic for large years: {stderr}" + ); + assert!( + !stderr.contains("panicked"), + "touch must not panic for large years: {stderr}" + ); + + // Keep the test workspace clean if the command succeeded. + let _ = remove_file(at.plus_as_string(file)); +} + #[test] fn test_touch_no_create_file_absent() { let (at, mut ucmd) = at_and_ucmd!();