diff --git a/Cargo.lock b/Cargo.lock index c28f80b..8704586 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,11 +2,26 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -20,12 +35,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.102" @@ -62,17 +121,32 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + [[package]] name = "bitflags" -version = "2.4.2" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "bstr" -version = "1.9.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", "regex-automata", @@ -87,9 +161,13 @@ checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" [[package]] name = "cc" -version = "1.0.90" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] [[package]] name = "cfg-if" @@ -142,8 +220,10 @@ version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99" dependencies = [ + "anstream", "anstyle", "clap_lex", + "strsim", "terminal_size", ] @@ -195,7 +275,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -213,6 +293,12 @@ dependencies = [ "regex-lite", ] +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "colored" version = "2.2.0" @@ -229,6 +315,26 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf0a07a401f374238ab8e2f11a104d2851bf9ce711ec69804834de8af45c7af" +[[package]] +name = "const_format" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -263,6 +369,7 @@ dependencies = [ "assert_cmd", "chrono", "codspeed-divan-compat", + "const_format", "diff", "itoa", "predicates", @@ -270,8 +377,21 @@ dependencies = [ "rand", "regex", "same-file", + "signals", "tempfile", "unicode-width", + "uucore", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -282,7 +402,7 @@ checksum = "8dc51d98e636f5e3b0759a39257458b22619cac7e96d932da6eeb052891bb67c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -307,12 +427,40 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "failure" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +dependencies = [ + "backtrace", + "failure_derive", +] + +[[package]] +name = "failure_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure", +] + [[package]] name = "fastrand" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "float-cmp" version = "0.10.0" @@ -322,6 +470,51 @@ dependencies = [ "num-traits", ] +[[package]] +name = "fluent" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8137a6d5a2c50d6b0ebfcb9aaa91a28154e0a70605f112d30cb0cd4a78670477" +dependencies = [ + "fluent-bundle", + "unic-langid", +] + +[[package]] +name = "fluent-bundle" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01203cb8918f5711e73891b347816d932046f95f54207710bda99beaeb423bf4" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash", + "self_cell", + "smallvec", + "unic-langid", +] + +[[package]] +name = "fluent-langneg" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eebbe59450baee8282d71676f3bfed5689aeab00b27545e83e5f14b1195e8b0" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "fluent-syntax" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198" +dependencies = [ + "memchr", + "thiserror", +] + [[package]] name = "foldhash" version = "0.1.5" @@ -353,6 +546,12 @@ dependencies = [ "wasip3", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + [[package]] name = "glob" version = "0.3.3" @@ -421,6 +620,31 @@ dependencies = [ "serde_core", ] +[[package]] +name = "intl-memoizer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f" +dependencies = [ + "type-map", + "unic-langid", +] + +[[package]] +name = "intl_pluralrules" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.14.0" @@ -483,9 +707,18 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "memchr" -version = "2.7.1" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] [[package]] name = "nix" @@ -507,19 +740,43 @@ checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "os_display" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad5fd71b79026fb918650dde6d125000a233764f1c2f1659a1c71118e33ea08f" +dependencies = [ + "unicode-width", +] + [[package]] name = "predicates" version = "3.1.4" @@ -567,7 +824,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.117", ] [[package]] @@ -590,9 +847,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.35" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -655,6 +912,18 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustix" version = "0.38.44" @@ -678,7 +947,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -690,6 +959,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + [[package]] name = "semver" version = "1.0.27" @@ -723,7 +998,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -739,6 +1014,27 @@ dependencies = [ "zmij", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signals" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5284a87265131b0829d6b7914ede19259a6f35f44d3e9939a570cd8f7ec6ee86" +dependencies = [ + "failure", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + [[package]] name = "statrs" version = "0.18.0" @@ -749,6 +1045,23 @@ dependencies = [ "num-traits", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.117" @@ -760,6 +1073,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + [[package]] name = "tempfile" version = "3.26.0" @@ -770,7 +1095,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -789,6 +1114,37 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "serde_core", + "zerovec", +] + [[package]] name = "toml_datetime" version = "1.0.0+spec-1.1.0" @@ -819,6 +1175,33 @@ dependencies = [ "winnow", ] +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash", +] + +[[package]] +name = "unic-langid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" +dependencies = [ + "tinystr", +] + [[package]] name = "unicode-ident" version = "1.0.12" @@ -837,6 +1220,40 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uucore" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b157ba598d7f7ed06f6dbc62999edb9d730b4d3fb58e503d8ad6d5fbe1e04391" +dependencies = [ + "clap", + "fluent", + "fluent-bundle", + "fluent-syntax", + "nix", + "os_display", + "thiserror", + "unic-langid", + "uucore_procs", + "wild", +] + +[[package]] +name = "uucore_procs" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daa291a52608ac5a2f8539e119666e021baa6b8c01f22f02ed201bbae54cbbc0" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "wait-timeout" version = "0.2.0" @@ -891,7 +1308,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -913,7 +1330,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -959,36 +1376,23 @@ dependencies = [ ] [[package]] -name = "winapi" -version = "0.3.9" +name = "wild" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +checksum = "a3131afc8c575281e1e80f36ed6a092aa502c08b18ed7524e86fbbb12bb410e1" dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", + "glob", ] -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "winapi", + "windows-sys 0.61.2", ] -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows-core" version = "0.52.0" @@ -1000,9 +1404,9 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" @@ -1022,6 +1426,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -1182,7 +1595,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -1198,7 +1611,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -1246,6 +1659,22 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "serde", + "zerofrom", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index 1673839..b413de7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,11 +16,14 @@ path = "src/main.rs" [dependencies] chrono = "0.4.38" +uucore = "0.6.0" +const_format = "0.2.35" diff = "0.1.13" itoa = "1.0.11" regex = "1.10.4" same-file = "1.0.6" unicode-width = "0.2.0" +signals = "0.0.5" [dev-dependencies] assert_cmd = "2.0.14" @@ -46,7 +49,14 @@ harness = false [features] # default = ["feat_bench_not_diff"] -# Turn bench for diffutils cmp off +# instead of limiting to KiB, MiB, etc, one can write kib, mib, Mb or whatever case. +feat_allow_case_insensitive_number_units = [] +# Disable bench for diffutils cmp feat_bench_not_cmp = [] -# Turn bench for diffutils diff off +# Disable bench for diffutils diff feat_bench_not_diff = [] +# Enables a check on options defined in NOT_YET_IMPLEMENTED. +# If on the parser will return an error message in these cases. +# This is preferable when running the util as unsupported options +# are pointed out to the user, but can make tests fail. +feat_check_not_yet_implemented = [] diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 545c6ec..9cdecc4 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.4" @@ -20,6 +35,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "arbitrary" version = "1.4.2" @@ -32,6 +97,27 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + [[package]] name = "bumpalo" version = "3.19.1" @@ -56,6 +142,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.42" @@ -69,6 +161,40 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clap" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", + "terminal_size", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "const_format" version = "0.2.35" @@ -111,7 +237,52 @@ dependencies = [ "itoa", "regex", "same-file", + "signals", "unicode-width", + "uucore", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.112", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "failure" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +dependencies = [ + "backtrace", + "failure_derive", +] + +[[package]] +name = "failure_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure", ] [[package]] @@ -120,6 +291,51 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" +[[package]] +name = "fluent" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8137a6d5a2c50d6b0ebfcb9aaa91a28154e0a70605f112d30cb0cd4a78670477" +dependencies = [ + "fluent-bundle", + "unic-langid", +] + +[[package]] +name = "fluent-bundle" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01203cb8918f5711e73891b347816d932046f95f54207710bda99beaeb423bf4" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash", + "self_cell", + "smallvec", + "unic-langid", +] + +[[package]] +name = "fluent-langneg" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eebbe59450baee8282d71676f3bfed5689aeab00b27545e83e5f14b1195e8b0" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "fluent-syntax" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198" +dependencies = [ + "memchr", + "thiserror", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -132,6 +348,18 @@ dependencies = [ "wasip2", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "iana-time-zone" version = "0.1.64" @@ -156,6 +384,31 @@ dependencies = [ "cc", ] +[[package]] +name = "intl-memoizer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f" +dependencies = [ + "type-map", + "unic-langid", +] + +[[package]] +name = "intl_pluralrules" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itoa" version = "1.0.17" @@ -184,9 +437,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.178" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libfuzzer-sys" @@ -198,6 +451,12 @@ dependencies = [ "cc", ] +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "log" version = "0.4.29" @@ -210,6 +469,27 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -219,12 +499,36 @@ dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "os_display" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad5fd71b79026fb918650dde6d125000a233764f1c2f1659a1c71118e33ea08f" +dependencies = [ + "unicode-width", +] + [[package]] name = "proc-macro2" version = "1.0.104" @@ -278,6 +582,31 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -293,12 +622,80 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.112", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signals" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5284a87265131b0829d6b7914ede19259a6f35f44d3e9939a570cd8f7ec6ee86" +dependencies = [ + "failure", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.112" @@ -310,6 +707,86 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + +[[package]] +name = "terminal_size" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +dependencies = [ + "rustix", + "windows-sys 0.60.2", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.112", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "serde_core", + "zerovec", +] + +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash", +] + +[[package]] +name = "unic-langid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" +dependencies = [ + "tinystr", +] + [[package]] name = "unicode-ident" version = "1.0.22" @@ -336,6 +813,40 @@ dependencies = [ "libfuzzer-sys", ] +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uucore" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b157ba598d7f7ed06f6dbc62999edb9d730b4d3fb58e503d8ad6d5fbe1e04391" +dependencies = [ + "clap", + "fluent", + "fluent-bundle", + "fluent-syntax", + "nix", + "os_display", + "thiserror", + "unic-langid", + "uucore_procs", + "wild", +] + +[[package]] +name = "uucore_procs" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daa291a52608ac5a2f8539e119666e021baa6b8c01f22f02ed201bbae54cbbc0" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" @@ -377,7 +888,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.112", "wasm-bindgen-shared", ] @@ -390,13 +901,22 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wild" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3131afc8c575281e1e80f36ed6a092aa502c08b18ed7524e86fbbb12bb410e1" +dependencies = [ + "glob", +] + [[package]] name = "winapi-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -420,7 +940,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.112", ] [[package]] @@ -431,7 +951,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.112", ] [[package]] @@ -458,6 +978,15 @@ 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" @@ -467,8 +996,89 @@ 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 = "wit-bindgen" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "serde", + "zerofrom", +] diff --git a/fuzz/fuzz_targets/fuzz_side.rs b/fuzz/fuzz_targets/fuzz_side.rs index 8a69c07..93768b2 100644 --- a/fuzz/fuzz_targets/fuzz_side.rs +++ b/fuzz/fuzz_targets/fuzz_side.rs @@ -4,9 +4,9 @@ extern crate libfuzzer_sys; use diffutilslib::side_diff; +use diffutilslib::params::Params; use std::fs::File; use std::io::Write; -use diffutilslib::params::Params; fuzz_target!(|x: (Vec, Vec, /* usize, usize */ bool)| { let (original, new, /* width, tabsize, */ expand) = x; @@ -22,7 +22,7 @@ fuzz_target!(|x: (Vec, Vec, /* usize, usize */ bool)| { ..Default::default() }; let mut output_buf = vec![]; - side_diff::diff(&original, &new, &mut output_buf, ¶ms); + side_diff::diff(&original, &new, &mut output_buf, &(¶ms).into()); File::create("target/fuzz.file.original") .unwrap() .write_all(&original) @@ -39,4 +39,4 @@ fuzz_target!(|x: (Vec, Vec, /* usize, usize */ bool)| { .unwrap() .write_all(&output_buf) .unwrap(); -}); \ No newline at end of file +}); diff --git a/src/arg_parser.rs b/src/arg_parser.rs new file mode 100644 index 0000000..0cf1606 --- /dev/null +++ b/src/arg_parser.rs @@ -0,0 +1,909 @@ +// This file is part of the uutils diffutils package. +// +// For the full copyright and license information, please view the LICENSE-* +// files that was distributed with this source code. + +//! This is a generic parser for program arguments (operands and options). +//! +//! The [Parser] is able to parse the options of all diffutils, e.g. `cmp --options` or `diff --options`. +//! +//! Features: +//! +//! - Allows options to be abbreviated, e.g. \--wi instead of \--width +//! - Allows input like in GNU utils, e.g. the following are all identical: +//! - `diff --ignore-case --minimal --width=50 file_a file_b` +//! - `diff --ignore-case --minimal --width 50 file_a file_b` +//! - `diff -i -d -w 50 file_a file_b` +//! - `diff -id -w50 file_a file_b` +//! - `diff -idw50 file_a file_b` +//! - A [NumberParser] is available, which parses option arguments +//! with optional byte units, e.g. =1024 or =1024KiB +//! - Default handling for \--version and \--help +//! - Returns the [ParsedOption]s or a [ParseError] Enum, which makes it library friendly. +//! - Contains error handling for the typical parsing errors: +//! - missing and extra operands +//! - invalid, ambiguous or conflicting options +//! - missing or not allowed option arguments +//! - Provides error text functions, e.g. add executable and 'Try \--help' to message. +//! +use std::{ + error::Error, + ffi::{OsStr, OsString}, + fmt::Display, + iter::Peekable, +}; + +// TODO finalize copyright +pub const TEXT_COPYRIGHT: &str = r#"Copyright (c) uutils developers +Licenses: MIT License, Apache License 2.0 "#; + +// TODO finalize help text footer +pub const TEXT_HELP_FOOTER: &str = r#" +This utility is part of the Rust uutils project: https://github.com/uutils/. +Report bugs here: https://github.com/uutils/diffutils/issues. +"#; + +// Version text +#[allow(unused)] +pub const TEXT_VERSION_BASE: &str = concat!("(uutils diffutils) ", env!("CARGO_PKG_VERSION"),); + +// AppOption for help, also reacting on -h +pub const OPT_HELP: AppOption = AppOption { + long_name: "help", + short: Some('h'), + has_arg: false, +}; +pub const OPT_VERSION: AppOption = AppOption { + long_name: "version", + short: Some('v'), + has_arg: false, +}; + +/// Add a centralized copyright message to another text. +pub fn add_copyright(text: &str) -> String { + format!("{text}\n{TEXT_COPYRIGHT}") +} + +/// Writes the error message and adds the help hint "Try 'diff \--help' for more information." +/// +/// * exe: [Executable] +/// * msg: The message to output. It will be preceded by 'executable: '. +/// Sometimes the executable is not available during error message creation, +/// so #EXE will be replaced by the name of the executable, e.g. 'diff'. +/// +/// This is the central output function. I affects all utils. \ +/// It allows to just use 'eprintln!("{e}");' in case of an error. +pub fn format_error_text(executable: &Executable, error: &T) -> String { + // for messages the have the executable already + let exe = format!("{executable}: "); + let msg = error + .to_string() + .replace("#EXE", executable.to_string().as_str()); + if msg.starts_with(&exe) { + format!("{msg}\n{exe}Try '{executable} --help' for more information.",) + } else { + format!("{exe}{msg}\n{exe}Try '{executable} --help' for more information.",) + } +} + +/// Returns the standardized version text for this utility. +pub fn get_version_text(executable: &Executable) -> String { + format!("{executable} {TEXT_VERSION_BASE}") +} + +/// Convert a text into input for the parsers. +/// +/// This is for testing and allows to write a simple string `diff file_1 file_2 --width=50` +/// to be converted in the input format the parser expects, like ArgsOs. +#[allow(unused)] +pub fn args_into_peekable_os_strings(args: &str) -> Peekable> { + let mut o = Vec::new(); + for arg in args.split(' ') { + o.push(OsString::from(arg)); + } + o.into_iter().peekable() +} + +/// Check if the user selected an option which is not yet implemented. +#[allow(unused)] +pub fn is_implemented( + options_parsed: &[ParsedOption], + implemented_options: &[AppOption], +) -> Result<(), ParseError> { + if let Some(not_yet) = options_parsed + .iter() + .find(|o| implemented_options.contains(o.app_option)) + { + return Err(ParseError::NotYetImplemented(format!( + "'--{}' (-{})", + not_yet.app_option.long_name, + not_yet.app_option.short.unwrap_or(' ') + ))); + } + + Ok(()) +} + +/// This contains the args/options the app allows. They must be all of const value. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct AppOption { + /// long name of option + pub long_name: &'static str, + pub short: Option, + pub has_arg: bool, + // pub arg_default: Option<&'static str>, +} + +impl AppOption { + /// formatted long option + /// + /// Returns the long name formatted: `--option`. \ + /// There is inconsistency in GNU diffutils, if these are printed with or without quotes. + pub fn format_long(&self) -> String { + format!("--{}", self.long_name) + } + + /// formatted long and short option + /// + /// There is inconsistency in GNU diffutils, if these are printed with or without quotes. + /// + /// # Returns + /// * Some(short): `'--option' (-c)`. + /// * None: [Self::format_long] + pub fn format_for_error_msg(&self) -> String { + self.format_long() + // match self.short { + // Some(c) => format!("--{} (-{c})", self.long_name), + // None => self.format_long(), + // } + } + + /// formatted option char + /// + /// Returns the short char formatted: "-c" or an empty String if None. + #[allow(unused)] + pub fn short_or_empty(&self) -> String { + match self.short { + Some(c) => format!("-{c}"), + None => String::new(), + } + } +} + +/// One parsed option. +#[derive(Debug, Clone, PartialEq)] +pub struct ParsedOption { + pub app_option: &'static AppOption, + /// Argument of the option as string_lossy, e.g. the "1000kB" of "\--bytes=1000kB". + pub arg_for_option: Option, + /// Argument of the option as original OsString + pub arg_for_option_os: Option, + /// If the user typed the long name or used the short char to set the option. + pub name_type_used: OptionNameTypeUsed, +} + +impl ParsedOption { + pub fn new( + app_option: &'static AppOption, + arg_for_option_os: OsString, + name_type_used: OptionNameTypeUsed, + ) -> Self { + Self { + app_option, + arg_for_option: Some(arg_for_option_os.to_string_lossy().to_string()), + arg_for_option_os: Some(arg_for_option_os), + name_type_used, + } + } + + /// Create an option which does not have an argument. + pub fn new_no_arg(app_option: &'static AppOption, used: OptionNameTypeUsed) -> Self { + Self { + app_option, + arg_for_option: None, + arg_for_option_os: None, + name_type_used: used, + } + } + + /// This checks if an option requires an argument and if it already known. + /// + /// * Case A: `--long-option=argument`: Argument is already parsed + /// * Case B: `--long-option argument`: Argument must be the next in the given args + /// * Case C: `-bArgument`: Argument is already parsed + /// * Case D: `-b Argument`: Argument must be the next in the given args + fn check_add_arg>( + &mut self, + opts: &mut Peekable, + ) -> Result<(), ParseError> { + // argument missing + if self.app_option.has_arg { + if self.arg_for_option.is_none() { + // take following argument if it is not an option + if let Some(arg) = opts.peek() { + let arg = arg.to_string_lossy(); + if !arg.starts_with('-') { + self.arg_for_option = Some(arg.to_string()); + _ = opts.next(); + } + } + if self.arg_for_option.is_none() { + return Err(ParseError::ArgForOptionMissing(self.clone())); + } + } + } else { + // argument allowed? + if self.arg_for_option.is_some() { + return Err(ParseError::ArgForOptionNotAllowed(self.clone())); + } + } + + Ok(()) + } + + /// Sets arg_for_option_os and arg_for_option as string_lossy. + pub fn set_arg_for_option(&mut self, arg_for_option_os: OsString) { + self.arg_for_option = Some(arg_for_option_os.to_string_lossy().to_string()); + self.arg_for_option_os = Some(arg_for_option_os); + } + + /// Easy String conversion: returns the Argument or an empty String if None. + pub fn arg_for_option_or_empty_string(&self) -> String { + match &self.arg_for_option { + Some(s) => s.clone(), + None => String::new(), + } + } +} + +impl Default for ParsedOption { + fn default() -> Self { + Self { + app_option: &AppOption { + long_name: "dummy", + short: None, + has_arg: false, + }, + arg_for_option: None, + arg_for_option_os: None, + name_type_used: OptionNameTypeUsed::LongName, + } + } +} + +/// To differentiate the user input, did he use -s or \--silent. +/// While this is technically no difference, the error message may vary. +#[derive(Debug, Default, Clone, Copy, PartialEq)] +pub enum OptionNameTypeUsed { + #[default] + None, + LongName, + ShortName, +} + +/// This is a generic parser for program arguments (operands and options), +/// but without the executable. +/// +/// This generic parser is able to parse the options of all diffutils, e.g. `cmp --options` or `diff --options`. \ +/// The allowed options are passed as a list of static [AppOption]s, as they are known at compile time. +/// +/// # Example: read params for sdiff +/// ```rust +/// # use diffutilslib::sdiff::{sdiff, SDiffOk, TEXT_HELP}; +/// # use diffutilslib::sdiff::params_sdiff; +/// let args = "sdiff --help"; +/// // let args = "sdiff file_1.txt file_2.txt --width=40"; +/// // Test helper conversion, usually this is ArgsOs. +/// let args = diffutilslib::arg_parser::args_into_peekable_os_strings(&args); +/// let params = match sdiff(args) { +/// Ok(res) => match res { +/// SDiffOk::Different => todo!(), +/// SDiffOk::Equal => todo!(), +/// SDiffOk::Help => { +/// println!("{TEXT_HELP}"); +/// return; // ExitCode::from(0); +/// } +/// SDiffOk::Version => todo!(), +/// }, +/// Err(e) => { +/// eprintln!("{e}"); +/// return; // ExitCode::from(2); +/// } +/// }; +/// ``` +#[derive(Debug, Default)] +pub struct Parser { + pub options_parsed: Vec, + pub operands: Vec, + // temporary stored for each param + name_type_used: OptionNameTypeUsed, +} + +impl Parser { + /// Parse the args into operands and options for the utility. + /// + /// The arguments must not contain the executable. + /// + /// The allowed options are passed as a list of static [AppOption]s, as they are known at compile time. + /// + /// # Returns Result + /// * Ok: [Parser] with [ParsedOption]s and operands (file names) + /// * Error: [ParseError] + pub fn parse_params>( + app_options: &'static [AppOption], + mut args: Peekable, + ) -> Result { + // sdiff options begin with ‘-’, so normally from-file and to-file may not begin with ‘-’. + // However, -- as an argument by itself treats the remaining arguments as file names even if they begin with ‘-’. + // You may not use - as an input file. + // read next param as file name, here we generally use read as operand + let mut parser = Self::default(); + let mut is_double_dash = false; + while let Some(param_os) = args.next() { + let mut param = param_os.to_string_lossy().to_string(); + // dbg!(¶m); + let mut ci = param.char_indices().peekable(); + let (_, c0) = ci.next().expect("Param must have at least one char!"); + // is param? + if c0 != '-' || param == "-" || is_double_dash { + // Operand, not an option with - or -- + // or single dash '-', this is for file as StandardInput + parser.operands.push(param_os); + continue; + } + // check 2nd char, which must exist, see above checks + let (_, c1) = ci.next().unwrap(); + let mut p_opt = ParsedOption::default(); + // has 3rd char? + if let Some((pos_c2, _c2)) = ci.peek() { + if c1 == '-' { + // long option, e.g. --bytes + parser.name_type_used = OptionNameTypeUsed::LongName; + + // Find argument for some options, either '=' or following arg. + // This also shortens param to its name. + if let Some(p) = param[*pos_c2..].find('=') { + // only --bytes and --ignore-initial must have bytes, else return error + // reduce param to option and + // return bytes without = sign. + let os = Self::split_os_prefix(¶m_os, p + *pos_c2 + 1)?; + p_opt.set_arg_for_option(os); + param = param[0..p + *pos_c2].to_string(); + } + + // allow partial option descriptors, like --he for --help, if unique + p_opt.app_option = + Self::identify_option_from_partial_text(¶m_os, app_options)?; + + p_opt.name_type_used = OptionNameTypeUsed::LongName; + p_opt.check_add_arg(&mut args)?; + parser.options_parsed.push(p_opt); + } else { + // -MultiSingleChar, e.g. -bl or option with bytes -n200 + parser.name_type_used = OptionNameTypeUsed::ShortName; + let mut c = c1; + let mut pos = 1; + loop { + let Some(opt) = app_options.iter().find(|o| o.short == Some(c)) else { + return Err(ParseError::InvalidOption(param_os)); + }; + if opt.has_arg { + // take rest of the string as arg + let arg_for_option_os = if param.len() > pos + 1 { + Some(Self::split_os_prefix(¶m_os, pos + 1)?) + } else { + args.next() + }; + let Some(os) = arg_for_option_os else { + return Err(ParseError::ArgForOptionMissing( + ParsedOption::new_no_arg(opt, OptionNameTypeUsed::ShortName), + )); + }; + parser.options_parsed.push(ParsedOption::new( + opt, + os, + OptionNameTypeUsed::ShortName, + )); + break; + } else { + parser + .options_parsed + .push(ParsedOption::new_no_arg(opt, OptionNameTypeUsed::ShortName)); + } + match ci.next() { + Some((p, cx)) => { + c = cx; + pos = p + } + None => break, + } + } + } + } else { + // single short options, e.g. -b. + parser.name_type_used = OptionNameTypeUsed::ShortName; + match app_options.iter().find(|opt| { + if let Some(c) = opt.short { + c == c1 + } else { + false + } + }) { + Some(opt) => { + p_opt.app_option = opt; + p_opt.name_type_used = OptionNameTypeUsed::ShortName; + p_opt.check_add_arg(&mut args)?; + parser.options_parsed.push(p_opt); + } + None => { + if c1 == '-' { + is_double_dash = true + } else { + return Err(ParseError::InvalidOption(param_os)); + } + } + } + } + } + + // identified unique option + if parser.is_help() { + parser.set_only_option(&OPT_HELP); + return Ok(parser); + } + if parser.is_version() { + parser.set_only_option(&OPT_VERSION); + return Ok(parser); + } + + Ok(parser) + } + + /// * param_os: expected to start with "\--" + pub fn identify_option_from_partial_text( + param_os: &OsStr, + app_options: &'static [AppOption], + ) -> Result<&'static AppOption, ParseError> { + assert!(param_os.len() > 2); + let mut param = ¶m_os.to_string_lossy()[2..]; + if let Some(p) = param.find('=') { + param = ¶m[0..p]; + } + let l = param.len(); + let possible_opts: Vec<&'static AppOption> = app_options + .iter() + .filter(|&it| it.long_name.len() >= l && &it.long_name[0..l] == param) + .collect(); + + match possible_opts.len() { + 0 => Err(ParseError::UnrecognizedOption(param_os.to_os_string())), + + 1 => Ok(*possible_opts.first().unwrap()), + + _ => Err(ParseError::AmbiguousOption( + param_os.to_os_string(), + possible_opts, + )), + } + } + + /// Check if user requested the \--help output. + pub fn is_help(&self) -> bool { + self.options_parsed + .iter() + .any(|opt| *opt.app_option == OPT_HELP) + } + + /// Check if user requested the \--version output. + pub fn is_version(&self) -> bool { + self.options_parsed + .iter() + .any(|opt| *opt.app_option == OPT_VERSION) + } + + fn set_only_option(&mut self, option: &'static AppOption) { + self.options_parsed = vec![ParsedOption::new_no_arg(option, self.name_type_used)]; + self.operands.clear(); + } + + /// Split an OsString on Linux. On Windows this is not possible. \ + /// This is required for options like `--file-name=argument-non-utf-8` + /// + /// # Returns + /// * A slice of the OsStr starting from `index`. + /// * None if the OS doesn't support byte-slicing or index is out of bounds. + pub fn split_os_prefix(os_str: &OsStr, index: usize) -> Result { + #[cfg(unix)] + { + // On Unix, OsStr is just a sequence of bytes (often UTF-8, but not guaranteed). + use std::os::unix::ffi::OsStrExt; + let bytes = os_str.as_bytes(); + if index <= bytes.len() { + return Ok(OsStr::from_bytes(&bytes[index..]).to_os_string()); + } + } + + #[cfg(not(unix))] + { + // On Windows/others, we can't safely slice raw bytes because + // they use Wtf-8/Utf-16 which might split a surrogate pair. + // We fall back to UTF-8 conversion if possible. + let r = os_str.to_str().and_then(|s| { + if index <= s.len() { + Some(OsString::from(&s[index..])) + } else { + None + } + }); + if let Some(os) = r { + return Ok(os); + } + } + + Err(ParseError::NoUnicode(os_str.to_os_string())) + } +} + +/// Contains all parser errors and their text messages. +/// +/// All errors can be output easily using the normal Display functionality. +/// To format the error message for the typical diffutils output, use [format_error_text]. +#[derive(Debug, Clone, PartialEq)] +pub enum ParseError { + /// When the long option is abbreviated, but does not have a unique match. + /// (ambiguous option, possible options) + AmbiguousOption(OsString, Vec<&'static AppOption>), + + /// 'executable': option '--silent' doesn't allow an argument + /// (wrong option) + ArgForOptionNotAllowed(ParsedOption), + + /// (option, short or long name used) + ArgForOptionMissing(ParsedOption), + + /// Having more operands than allowed (usually 2) + /// (wrong operand) + ExtraOperand(OsString), + + /// Non-existent single dash option. + /// (unidentified option) + InvalidOption(OsString), + + /// number for an option argument incorrect + InvalidValueNumber(ParsedOption), + #[allow(unused)] // Allow external usage (cmp) + InvalidValueNumberUnit(ParsedOption), + #[allow(unused)] // Allow external usage (cmp) + InvalidValueNumberOverflow(ParsedOption), + + /// 'executable' as first parameter missing. + #[allow(unused)] // Allow usage for main function so all parsing errors are covered. + NoExecutable, + + /// no args for the actual utility given + NoOperands(Executable), + + /// Parsed option is not in unicode. + /// Since Rust cannot split OsString on Non-Linux Systems, + /// it can accept the argument for an option only as + /// separate arg (--regex someRegex). + NoUnicode(OsString), + + /// Two options cannot be used together, e.g. cmp --silent and --verbose (output). + #[allow(unused)] // Allow external usage (cmp) + OptionsIncompatible(&'static AppOption, &'static AppOption), + + /// Non-existent long option. This is "unrecognized" because the name can be abbreviated. + /// (unrecognized option) + UnrecognizedOption(OsString), + + NotYetImplemented(String), +} + +impl Display for ParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ParseError::AmbiguousOption(param, possible_opts) => { + // create list of possible options + let mut list = Vec::new(); + for opt in possible_opts { + list.push(opt.format_long()); + } + write!( + f, + "option '{}' is ambiguous; possibilities: {}", + param.to_string_lossy(), + list.join(" ") + ) + } + ParseError::ArgForOptionNotAllowed(opt) => write!( + f, + "option {} doesn't allow an argument", + opt.app_option.format_long() + ), + ParseError::ArgForOptionMissing(opt) => { + write!( + f, + "option {} requires an argument", + opt.app_option.format_for_error_msg(), + ) + } + ParseError::ExtraOperand(opt) => write!(f, "extra operand '{}'", opt.to_string_lossy()), + ParseError::InvalidValueNumber(opt) | ParseError::InvalidValueNumberUnit(opt) => { + write!( + f, + "invalid {} value '{}'", + // "invalid argument '{}' for '--{}'{}", + opt.app_option.format_for_error_msg(), + opt.arg_for_option_or_empty_string(), + // opt.short_char_or_empty_string(), + ) + } + ParseError::InvalidValueNumberOverflow(opt) => { + write!( + f, + "invalid {} value '{}' (too large)", + // "invalid argument '{}' for '--{}'{}", + opt.app_option.format_for_error_msg(), + opt.arg_for_option_or_empty_string(), + // opt.short_char_or_empty_string(), + ) + } + ParseError::InvalidOption(param) => { + write!( + f, + "{}", + &format!("invalid option '{}'", param.to_string_lossy()) + ) + } + ParseError::NoExecutable => { + write!(f, "Expected utility name as second argument, got nothing.") + } + ParseError::NoOperands(exe) => { + write!(f, "missing operand after '{exe}'") + } + ParseError::NoUnicode(os) => { + let mut s = OsString::from("Cannot parse non-unicode '"); + s.push(os); + s.push(OsString::from( + "'. Separate the argument from the option, e.g. '--from-file argument' instead '--from-file=argument'", + )); + write!(f, "Expected utility name as second argument, got nothing.") + } + ParseError::OptionsIncompatible(op_1, op_2) => { + write!( + f, + "options {} and {} are incompatible", + op_1.format_for_error_msg(), + op_2.format_for_error_msg() + ) + } + ParseError::UnrecognizedOption(param) => { + write!( + f, + "{}", + &format!("unrecognized option '{}'", param.to_string_lossy()) + ) + } + ParseError::NotYetImplemented(param) => { + write!(f, "{}", &format!("not yet implemented: option {param}")) + } + } + } +} + +#[allow(unused)] // required for cmp +pub struct NumberParser {} + +#[allow(unused)] // required for cmp +impl NumberParser { + /// Parses a number with an optional unit, e.g. 10MiB. + /// + /// Follows . + pub fn parse_number(parsed_option: &ParsedOption) -> Result { + let Some(num_unit) = &parsed_option.arg_for_option else { + return Err(ParseError::InvalidValueNumber(parsed_option.clone())); + }; + if num_unit.is_empty() { + return Err(ParseError::InvalidValueNumber(parsed_option.clone())); + } + + // split number and unit, parse unit + let multiplier: u64; + let n = match num_unit.find(|b: char| !b.is_ascii_digit()) { + Some(pos) => { + if pos == 0 { + return Err(ParseError::InvalidValueNumber(parsed_option.clone())); + } + multiplier = Self::parse_number_unit(&num_unit[pos..], parsed_option)?; + &num_unit[0..pos] + } + None => { + multiplier = 1; + num_unit + } + }; + + // return value + match n.parse::() { + Ok(num) => { + if multiplier == 1 { + Ok(num) + } else { + match num.checked_mul(multiplier) { + Some(r) => Ok(r), + None => Err(ParseError::InvalidValueNumberOverflow( + parsed_option.clone(), + )), + } + } + } + // This is an additional error message not present in GNU DiffUtils. + Err(e) if *e.kind() == std::num::IntErrorKind::PosOverflow => Err( + ParseError::InvalidValueNumberOverflow(parsed_option.clone()), + ), + Err(_) => Err(ParseError::InvalidValueNumber(parsed_option.clone())), + } + } + + /// Parses a number unit, e.g. "KiB" into a multiplier + /// which then can be used to calculate the final number of bytes. + /// + /// # Returns + /// A multiplier depending on the given unit, e.g. 'KiB' -> 1024 + /// or None if unit could not be identified. + /// + /// Units up eo Exabyte (EiB) following GNU documentation: \ + /// . + #[cfg(not(feature = "feat_allow_case_insensitive_number_units"))] + fn parse_number_unit(unit: &str, parsed_option: &ParsedOption) -> Result { + let multiplier = match unit { + "kB" | "KB" => 1_000, + "k" | "K" | "KiB" | "kiB" => 1_024, + "MB" => 1_000_000, + "M" | "MiB" => 1_048_576, + "GB" => 1_000_000_000, + "G" | "GiB" => 1_073_741_824, + + "TB" => 1_000_000_000_000, + "T" | "TiB" => 1_099_511_627_776, + "PB" => 1_000_000_000_000_000, + "P" | "PiB" => 1_125_899_906_842_624, + "EB" => 1_000_000_000_000_000_000, + "E" | "EiB" => 1_152_921_504_606_846_976, + + // Everything above EiB cannot fit into u64. + // GNU cmp just returns an invalid bytes value + // "ZB" => 1_000_000_000_000_000_000_000, + // "Z" | "ZiB" => 1_180_591_620_717_411_303_424, + // "YB" => 1_000_000_000_000_000_000_000_000, + // "Y" | "YiB" => 1_208_925_819_614_629_174_706_176, + _ => { + return Err(ParseError::InvalidValueNumberUnit(parsed_option.clone())); + } + }; + + Ok(multiplier) + } + + /// Returns a multiplier depending on the given unit, e.g. 'KiB' -> 1024, + /// which then can be used to calculate the final number of bytes. + /// Following GNU documentation: https://www.gnu.org/software/diffutils/manual/html_node/cmp-Options.html + #[cfg(feature = "feat_allow_case_insensitive_number_units")] + fn parse_number_unit(unit: &str, parsed_option: &ParsedOption) -> Result { + // Note that GNU cmp advertises supporting up to Y, but fails if you try + // to actually use anything beyond E. + let unit = unit.to_owned().to_ascii_lowercase(); + // .to_ascii_lowercase().as_str(); + let multiplier = match unit.as_str() { + "kb" => 1_000, + "k" | "kib" => 1_024, + "mb" => 1_000_000, + "m" | "mib" => 1_048_576, + "gb" => 1_000_000_000, + "g" | "gib" => 1_073_741_824, + + "tb" => 1_000_000_000_000, + "t" | "tib" => 1_099_511_627_776, + "pb" => 1_000_000_000_000_000, + "p" | "pib" => 1_125_899_906_842_624, + "eb" => 1_000_000_000_000_000_000, + "e" | "eib" => 1_152_921_504_606_846_976, + + // Everything above EiB cannot fit into u64. + // GNU cmp just returns an invalid bytes value + // "zb" => 1_000_000_000_000_000_000_000, + // "z" | "zib" => 1_180_591_620_717_411_303_424, + // "yb" => 1_000_000_000_000_000_000_000_000, + // "y" | "yib" => 1_208_925_819_614_629_174_706_176, + _ => { + return Err(ParseError::InvalidValueNumberUnit(parsed_option.clone())); + } + }; + + Ok(multiplier) + } +} + +/// Differentiates the utilities included in DiffUtil +/// and replaces executable as OsString. +/// +/// This allows easy output of the executable name with +/// ```format!("{}", params.executable)``` +/// without calling ```to_string_lossy()``` each time. +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum Executable { + Cmp, + Diff, + Diff3, + Patch, + SDiff, + + // Called from a library. Stores name. + NotRecognized(OsString), +} + +#[allow(unused)] +impl Executable { + /// Returns the executable name as OsString. \ + /// + /// In case of [Self::NotRecognized], this is the original OsString. + /// + /// The name is mostly used to write it which always requires a String. + pub fn executable(&self) -> OsString { + match self { + Executable::NotRecognized(os_string) => os_string.clone(), + _ => OsString::from(self.to_string()), + } + } + + /// Return as OsString. Same as fn [Self::executable]. + pub fn to_os_string(&self) -> OsString { + self.executable() + } + + /// Read the first arg (the executable) without moving the iterator of args. + /// + /// Returns + /// - Some: [Executable]. + /// - Diffutils: diff, cmp, sdiff, diff3 and patch + /// - NotRecognized(OsString) for all other inputs + /// - None: only if no argument was found. + pub fn from_args_os>( + args: &mut Peekable, + move_iter: bool, + ) -> Option { + if move_iter { + args.next().map(|exe| Self::from(&exe)) + } else { + args.peek().map(Self::from) + } + } +} + +impl From<&OsString> for Executable { + fn from(executable: &OsString) -> Self { + match executable.to_str() { + Some("cmp") => Executable::Cmp, + Some("diff") => Executable::Diff, + Some("diff3") => Executable::Diff3, + Some("patch") => Executable::Patch, + Some("sdiff") => Executable::SDiff, + _ => Executable::NotRecognized(OsString::from(executable)), + } + } +} + +impl Display for Executable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let name = match self { + Executable::Cmp => "cmp", + Executable::Diff => "diff", + Executable::Diff3 => "diff3", + Executable::SDiff => "sdiff", + Executable::Patch => "patch", + Executable::NotRecognized(name) => &name.to_string_lossy(), + }; + write!(f, "{name}") + } +} diff --git a/src/diff.rs b/src/diff.rs index f4c0614..f5c2da3 100644 --- a/src/diff.rs +++ b/src/diff.rs @@ -12,6 +12,7 @@ use std::fs; use std::io::{self, stdout, Read, Write}; use std::iter::Peekable; use std::process::{exit, ExitCode}; +use uucore::error::FromIo; // Exit codes are documented at // https://www.gnu.org/software/diffutils/manual/html_node/Invoking-diff.html. @@ -81,7 +82,7 @@ pub fn main(opts: Peekable) -> ExitCode { }), Format::SideBySide => { let mut output = stdout().lock(); - side_diff::diff(&from_content, &to_content, &mut output, ¶ms) + side_diff::diff(&from_content, &to_content, &mut output, &(¶ms).into()) } }; if params.brief && !result.is_empty() { @@ -91,7 +92,26 @@ pub fn main(opts: Peekable) -> ExitCode { params.to.to_string_lossy() ); } else { - io::stdout().write_all(&result).unwrap(); + let result = io::stdout().write_all(&result); + match result { + // This code is taken from coreutils. + // + Ok(()) => {} + Err(err) if err.kind() == std::io::ErrorKind::BrokenPipe => { + // GNU seq prints the Broken pipe message but still exits with status 0 + // unless SIGPIPE was explicitly ignored, in which case it should fail. + let err = err.map_err_context(|| "write error".into()); + uucore::show_error!("{err}"); + #[cfg(unix)] + if uucore::signals::sigpipe_was_ignored() { + uucore::error::set_exit_code(1); + } + } + Err(err) => { + eprintln!("{err}"); + return ExitCode::FAILURE; + } + } } if result.is_empty() { maybe_report_identical_files(); diff --git a/src/lib.rs b/src/lib.rs index 342b01c..8dfa3cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,11 @@ +pub mod arg_parser; pub mod cmp; pub mod context_diff; pub mod ed_diff; pub mod macros; pub mod normal_diff; pub mod params; +pub mod sdiff; pub mod side_diff; pub mod unified_diff; pub mod utils; diff --git a/src/main.rs b/src/main.rs index b7c2712..c66e12f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,7 @@ use std::{ process::ExitCode, }; +mod arg_parser; mod cmp; mod context_diff; mod diff; @@ -18,6 +19,7 @@ mod ed_diff; mod macros; mod normal_diff; mod params; +mod sdiff; mod side_diff; mod unified_diff; mod utils; @@ -72,6 +74,7 @@ fn main() -> ExitCode { match util_name.to_str() { Some("diff") => diff::main(args), Some("cmp") => cmp::main(args), + Some("sdiff") => sdiff::main(args), Some(name) => { eprintln!("{name}: utility not supported"); ExitCode::from(2) diff --git a/src/sdiff.rs b/src/sdiff.rs new file mode 100644 index 0000000..a8869f7 --- /dev/null +++ b/src/sdiff.rs @@ -0,0 +1,253 @@ +// This file is part of the uutils diffutils package. +// +// For the full copyright and license information, please view the LICENSE-* +// files that was distributed with this source code. + +//! This module holds the core compare logic of sdiff. +pub mod params_sdiff; + +use std::{ + env::ArgsOs, + ffi::OsString, + fs, + io::{self, stdout, Read, Write}, + iter::Peekable, + process::ExitCode, +}; + +use crate::{ + arg_parser::{ + add_copyright, format_error_text, get_version_text, Executable, ParseError, + TEXT_HELP_FOOTER, + }, + sdiff::params_sdiff::{ParamsSDiff, SDiffParseOk}, + side_diff, utils, +}; + +// This contains the hard coded 'sdiff'. If required this needs to be replaced with the executable. +pub const TEXT_HELP: &str = const_format::concatcp!( + r#" + sdiff is a tool which allows to compare two text files for differences. + It outputs the differences in a side-by-side view. + Use 'diff' for a row-by-row view. + Use 'cmp' to compare binary files. + + Usage: sdiff [OPTIONS] [FILE]... + If a FILE is '-', read operating system's standard input. + + Options: + -o, --output=FILE operate interactively while sending output to FILE + --diff-program=PROGRAM use PROGRAM to compare files + -a, --text treat all files as text + -H, --speed-large-files assume large files with many scattered small changes + -d, --minimal try to find a smaller set of changes + + -i, --ignore-case do not distinguish between upper- and lower-case letters + -E, --ignore-tab-expansion ignore changes due to tab expansion + -Z, --ignore-trailing-space ignore white space at line end + -b, --ignore-space-change ignore changes in the amount of white space + -W, --ignore-all-space ignore all white space + -B, --ignore-blank-lines ignore changes whose lines are all blank + -I, --ignore-matching-lines=REGEX ignore changes all whose lines match REGEX expression + --strip-trailing-cr strip trailing carriage return on input + + -s, --suppress-common-lines do not output common lines + -l, --left-column output only the left column of common lines + -t, --expand-tabs expand tabs to spaces in output + --tabsize=NUM tab stops at every NUM (default 8) print columns + -w, --width=NUM limit the print width to NUM print columns (default 130) + + -h --help display this help and exit + -v, --version output version information and exit + + Exit status is 0 if inputs are identical, 1 if different, 2 in error case. + "#, + TEXT_HELP_FOOTER +); + +/// Entry into sdiff. +/// +/// Param options, e.g. 'sdiff file1.txt file2.txt -bd n2000kB'. \ +/// sdiff options as documented in the GNU manual. +/// +/// Ends program with Exit Status: +/// * 0 if inputs are identical +/// * 1 if inputs are different +/// * 2 in error case +pub fn main(mut args: Peekable) -> ExitCode { + let Some(executable) = Executable::from_args_os(&mut args, false) else { + eprintln!("Expected utility name as first argument, got nothing."); + return ExitCode::FAILURE; + }; + match sdiff(args) { + Ok(res) => match res { + SDiffOk::Different => ExitCode::FAILURE, + SDiffOk::Equal => ExitCode::SUCCESS, + SDiffOk::Help => { + println!("{}", add_copyright(TEXT_HELP)); + ExitCode::SUCCESS + } + SDiffOk::Version => { + println!("{}", get_version_text(&executable)); + ExitCode::SUCCESS + } + }, + Err(e) => { + let msg = match e { + SDiffError::ReadFileErrors(_, _) => { + format!("{e}") + } + _ => format_error_text(&executable, &e), + }; + // let msg = format_error_text(&executable, &e); + eprintln!("{msg}"); + ExitCode::from(2) + } + } +} + +/// This is the full sdiff call. +/// +/// The first arg needs to be the executable, then the operands and options. +pub fn sdiff>(mut args: Peekable) -> Result { + let Some(executable) = Executable::from_args_os(&mut args, true) else { + return Err(ParseError::NoExecutable.into()); + }; + // read params + let params = match ParamsSDiff::parse_params(&executable, args)? { + SDiffParseOk::Params(p) => p, + SDiffParseOk::Help => return Ok(SDiffOk::Help), + SDiffParseOk::Version => return Ok(SDiffOk::Version), + }; + // dbg!("{params:?}"); + + // compare files + sdiff_compare(¶ms) +} + +/// This is the main function to compare the files. \ +/// +/// TODO sdiff is missing a number of options, currently implemented: +/// * expand_tabs +/// * tabsize +/// * width +/// * The output format does not match GNU sdiff +pub fn sdiff_compare(params: &ParamsSDiff) -> Result { + if utils::is_same_file(¶ms.from, ¶ms.to) { + return Ok(SDiffOk::Equal); + } + + let (from_content, to_content) = match read_both_files(¶ms.from, ¶ms.to) { + Ok(files) => files, + Err(errors) => { + let mut vs = Vec::new(); + for (file, e) in errors { + let s = utils::format_failure_to_read_input_file( + ¶ms.executable.to_os_string(), + &file, + &e, + ); + vs.push(s); + } + return Err(SDiffError::ReadFileErrors( + params.executable.clone(), + vs.to_vec(), + )); + } + }; + + // run diff + let mut output = stdout().lock(); + let result = side_diff::diff(&from_content, &to_content, &mut output, ¶ms.into()); + + match std::io::stdout().write_all(&result) { + Ok(_) => { + if result.is_empty() { + Ok(SDiffOk::Equal) + } else { + Ok(SDiffOk::Different) + } + } + Err(e) => Err(SDiffError::OutputError(e.to_string())), + } +} + +/// Helper function to read a file fully into memory. +// While this could be in utils, the functionality is limited to files which fit into memory. +// TODO will not work for large files, need buffered approach. +pub fn read_file_contents(filepath: &OsString) -> io::Result> { + if filepath == "-" { + let mut content = Vec::new(); + io::stdin().read_to_end(&mut content).and(Ok(content)) + } else { + fs::read(filepath) + } +} + +/// Reads both files and returns the files or a list of errors, as both files can produce a separate error. +pub type ResultReadBothFiles = Result<(Vec, Vec), Vec<(OsString, io::Error)>>; +/// Reads both files and returns the files or a list of errors, as both files can produce a separate error. +pub fn read_both_files(from: &OsString, to: &OsString) -> ResultReadBothFiles { + let mut read_errors = Vec::new(); + let from_content = match read_file_contents(from).map_err(|e| (from.clone(), e)) { + Ok(r) => r, + Err(e) => { + read_errors.push(e); + Vec::new() + } + }; + let to_content = match read_file_contents(to).map_err(|e| (to.clone(), e)) { + Ok(r) => r, + Err(e) => { + read_errors.push(e); + Vec::new() + } + }; + + if read_errors.is_empty() { + Ok((from_content, to_content)) + } else { + Err(read_errors) + } +} + +/// The Ok result of sdiff. +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum SDiffOk { + Different, + Equal, + Help, + Version, +} + +/// Errors for sdiff. +/// +/// To centralize error messages and make it easier to use in a lib. +#[derive(Debug, Clone, PartialEq)] +#[allow(clippy::enum_variant_names)] +pub enum SDiffError { + // parse errors + ParseError(ParseError), + + // compare errors + OutputError(String), + ReadFileErrors(Executable, Vec), +} + +impl std::error::Error for SDiffError {} + +impl From for SDiffError { + fn from(e: ParseError) -> Self { + Self::ParseError(e) + } +} + +impl std::fmt::Display for SDiffError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SDiffError::ParseError(e) => write!(f, "{e}"), + SDiffError::OutputError(msg) => write!(f, "{msg}"), + SDiffError::ReadFileErrors(_exe, vec_err) => write!(f, "{}", vec_err.join("\n")), + } + } +} diff --git a/src/sdiff/params_sdiff.rs b/src/sdiff/params_sdiff.rs new file mode 100644 index 0000000..211cf83 --- /dev/null +++ b/src/sdiff/params_sdiff.rs @@ -0,0 +1,488 @@ +// This file is part of the uutils diffutils package. +// +// For the full copyright and license information, please view the LICENSE-* +// files that was distributed with this source code. + +//! This module contains the Parser for sdiff arguments ([ParamsSDiff::parse_params]). +use std::{ffi::OsString, iter::Peekable}; + +use crate::arg_parser::{ + AppOption, Executable, ParseError, ParsedOption, Parser, OPT_HELP, OPT_VERSION, +}; + +// use crate::{ +// arg_parser::{self, AppOption, Executable, ParseError, ParsedOption, OPT_HELP, OPT_VERSION}, +// sdiff::params_sdiff::ParamsSDiff, +// }; + +pub type ResultSDiffParse = Result; + +// AppOptions for sdiff +pub const OPT_DIFF_PROGRAM: AppOption = AppOption { + long_name: "diff-program", + short: None, + has_arg: true, +}; +pub const OPT_EXPAND_TABS: AppOption = AppOption { + long_name: "expand-tabs", + short: Some('t'), + has_arg: false, +}; +pub const OPT_IGNORE_ALL_SPACE: AppOption = AppOption { + long_name: "ignore-all-space", + short: Some('W'), + has_arg: false, +}; +pub const OPT_IGNORE_BLANK_LINES: AppOption = AppOption { + long_name: "ignore-blank-lines", + short: Some('B'), + has_arg: false, +}; +pub const OPT_IGNORE_CASE: AppOption = AppOption { + long_name: "ignore-case", + short: Some('i'), + has_arg: false, +}; +pub const OPT_IGNORE_MATCHING_LINES: AppOption = AppOption { + long_name: "ignore-matching-lines", + short: Some('I'), + has_arg: true, +}; +pub const OPT_IGNORE_SPACE_CHANGE: AppOption = AppOption { + long_name: "ignore-space-change", + short: Some('b'), + has_arg: false, +}; +pub const OPT_IGNORE_TAB_EXPANSION: AppOption = AppOption { + long_name: "ignore-tab-expansion", + short: Some('E'), + has_arg: false, +}; +pub const OPT_IGNORE_TRAILING_SPACE: AppOption = AppOption { + long_name: "ignore-trailing-space", + short: Some('Z'), + has_arg: false, +}; +pub const OPT_LEFT_COLUMN: AppOption = AppOption { + long_name: "left-column", + short: Some('l'), + has_arg: false, +}; +pub const OPT_MINIMAL: AppOption = AppOption { + long_name: "minimal", + short: Some('d'), + has_arg: false, +}; +pub const OPT_OUTPUT: AppOption = AppOption { + long_name: "output", + short: Some('o'), + has_arg: true, +}; +pub const OPT_SPEED_LARGE_FILES: AppOption = AppOption { + long_name: "speed-large-files", + short: Some('H'), + has_arg: false, +}; +pub const OPT_STRIP_TRAILING_CR: AppOption = AppOption { + long_name: "strip-trailing-cr", + short: None, + has_arg: false, +}; +pub const OPT_SUPPRESS_COMMON_LINES: AppOption = AppOption { + long_name: "suppress-common-lines", + short: Some('s'), + has_arg: false, +}; +pub const OPT_TABSIZE: AppOption = AppOption { + long_name: "tabsize", + short: None, + has_arg: true, +}; +pub const OPT_TEXT: AppOption = AppOption { + long_name: "text", + short: Some('a'), + has_arg: false, +}; +pub const OPT_WIDTH: AppOption = AppOption { + long_name: "width", + short: Some('w'), + has_arg: true, +}; + +// Array for ArgParser +pub const APP_OPTIONS: [AppOption; 20] = [ + OPT_DIFF_PROGRAM, + OPT_EXPAND_TABS, + OPT_HELP, + OPT_IGNORE_ALL_SPACE, + OPT_IGNORE_BLANK_LINES, + OPT_IGNORE_CASE, + OPT_IGNORE_MATCHING_LINES, + OPT_IGNORE_SPACE_CHANGE, + OPT_IGNORE_TAB_EXPANSION, + OPT_IGNORE_TRAILING_SPACE, + OPT_LEFT_COLUMN, + OPT_MINIMAL, + OPT_OUTPUT, + OPT_SPEED_LARGE_FILES, + OPT_STRIP_TRAILING_CR, + OPT_SUPPRESS_COMMON_LINES, + OPT_TABSIZE, + OPT_TEXT, + OPT_VERSION, + OPT_WIDTH, +]; + +// These options throw an error, rather than go unnoticed. +#[cfg(feature = "feat_check_not_yet_implemented")] +pub const NOT_YET_IMPLEMENTED: [AppOption; 15] = [ + OPT_DIFF_PROGRAM, + OPT_IGNORE_ALL_SPACE, + OPT_IGNORE_BLANK_LINES, + OPT_IGNORE_CASE, + OPT_IGNORE_MATCHING_LINES, + OPT_IGNORE_SPACE_CHANGE, + OPT_IGNORE_TAB_EXPANSION, + OPT_IGNORE_TRAILING_SPACE, + OPT_LEFT_COLUMN, + OPT_MINIMAL, + OPT_OUTPUT, + OPT_SPEED_LARGE_FILES, + OPT_STRIP_TRAILING_CR, + OPT_SUPPRESS_COMMON_LINES, + OPT_TEXT, +]; + +/// Parser Result Ok Enum with Params. +/// +/// # Returns +/// - Params in normal cases +/// - Just Help or Version when these are requested as the params are then not relevant. +/// +/// Error will be returned as [ParseError] in the function Result Error. +#[derive(Debug, PartialEq)] +pub enum SDiffParseOk { + Params(ParamsSDiff), + Help, + Version, +} + +/// Holds the given command line arguments except "--version" and "--help". +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct ParamsSDiff { + /// Identifier + pub executable: Executable, + pub from: OsString, + pub to: OsString, + /// --diff-program=PROGRAM use PROGRAM to compare files + pub diff_program: Option, + /// -t, --expand-tabs expand tabs to spaces in output + pub expand_tabs: bool, + /// --help display this help and exit + pub help: bool, + /// -W, --ignore-all-space ignore all white space + pub ignore_all_space: bool, + /// -B, --ignore-blank-lines ignore changes whose lines are all blank + pub ignore_blank_lines: bool, + /// -i, --ignore-case consider upper- and lower-case to be the same + pub ignore_case: bool, + /// -I, --ignore-matching-lines=REGEXP ignore changes all whose lines match REGEXP + pub ignore_matching_lines: Option, + /// -b, --ignore-space-change ignore changes in the amount of white space + pub ignore_space_change: bool, + /// -E, --ignore-tab-expansion ignore changes due to tab expansion + pub ignore_tab_expansion: bool, + /// -Z, --ignore-trailing-space ignore white space at line end + pub ignore_trailing_space: bool, + /// -l, --left-column output only the left column of common lines + pub left_column: bool, + /// -d, --minimal try hard to find a smaller set of changes + pub minimal: bool, + /// -o, --output=FILE operate interactively, sending output to FILE + pub output: Option, + /// -H, --speed-large-files assume large files, many scattered small changes + pub speed_large_files: bool, + /// --strip-trailing-cr strip trailing carriage return on input + pub strip_trailing_cr: bool, + /// -s, --suppress-common-lines do not output common lines + pub suppress_common_lines: bool, + /// --tabsize=NUM tab stops at every NUM (default 8) print columns + pub tabsize: usize, + /// -a, --text treat all files as text + pub text: bool, + /// -v, --version output version information and exit + pub version: bool, + /// -w, --width=NUM output at most NUM (default 130) print columns + pub width: usize, +} + +impl Default for ParamsSDiff { + fn default() -> Self { + Self { + executable: Executable::SDiff, + from: Default::default(), + to: Default::default(), + diff_program: Default::default(), + expand_tabs: Default::default(), + help: Default::default(), + ignore_all_space: Default::default(), + ignore_blank_lines: Default::default(), + ignore_case: Default::default(), + ignore_matching_lines: Default::default(), + ignore_space_change: Default::default(), + ignore_tab_expansion: Default::default(), + ignore_trailing_space: Default::default(), + left_column: Default::default(), + minimal: Default::default(), + output: Default::default(), + speed_large_files: Default::default(), + strip_trailing_cr: Default::default(), + suppress_common_lines: Default::default(), + tabsize: 8, + text: Default::default(), + version: Default::default(), + width: 130, + } + } +} + +impl ParamsSDiff { + /// Parses the program arguments. + /// + /// The arguments must not contain the executable. + pub fn parse_params>( + executable: &Executable, + args: Peekable, + ) -> ResultSDiffParse { + let parser = Parser::parse_params(&APP_OPTIONS, args)?; + + // check implemented options + #[cfg(feature = "feat_check_not_yet_implemented")] + { + crate::arg_parser::is_implemented(&parser.options_parsed, &NOT_YET_IMPLEMENTED)?; + } + + let mut params = Self { + executable: executable.clone(), + ..Default::default() + }; + + // set options + for parsed_option in &parser.options_parsed { + // dbg!(parsed_option); + match *parsed_option.app_option { + OPT_DIFF_PROGRAM => params.diff_program = parsed_option.arg_for_option.clone(), + OPT_EXPAND_TABS => params.expand_tabs = true, + OPT_HELP => return Ok(SDiffParseOk::Help), + OPT_IGNORE_ALL_SPACE => params.ignore_all_space = true, + OPT_IGNORE_BLANK_LINES => params.ignore_blank_lines = true, + OPT_IGNORE_CASE => params.ignore_case = true, + OPT_IGNORE_MATCHING_LINES => { + params.ignore_matching_lines = parsed_option.arg_for_option.clone() + } + OPT_IGNORE_SPACE_CHANGE => params.ignore_space_change = true, + OPT_IGNORE_TAB_EXPANSION => params.ignore_tab_expansion = true, + OPT_IGNORE_TRAILING_SPACE => params.ignore_trailing_space = true, + OPT_LEFT_COLUMN => params.left_column = true, + OPT_MINIMAL => params.minimal = true, + OPT_OUTPUT => params.output = parsed_option.arg_for_option.clone(), + OPT_SPEED_LARGE_FILES => params.speed_large_files = true, + OPT_STRIP_TRAILING_CR => params.strip_trailing_cr = true, + OPT_SUPPRESS_COMMON_LINES => params.suppress_common_lines = true, + OPT_TABSIZE => { + params.set_tabsize(parsed_option)?; + } + OPT_TEXT => params.text = true, + OPT_VERSION => return Ok(SDiffParseOk::Version), + OPT_WIDTH => { + params.set_width(parsed_option)?; + } + + // This is not an error, but a todo. Unfortunately an Enum is not possible. + _ => todo!("Err Option: {}", parsed_option.app_option.long_name), + } + } + + // set operands + match parser.operands.len() { + 0 => return Err(ParseError::NoOperands(executable.clone())), + // If only file_1 is set, then file_2 defaults to '-', so it reads from StandardInput. + 1 => { + params.from = parser.operands[0].clone(); + params.to = OsString::from("-"); + } + 2 => { + params.from = parser.operands[0].clone(); + params.to = parser.operands[1].clone(); + } + _ => { + return Err(ParseError::ExtraOperand(parser.operands[2].clone())); + } + } + + // dbg!(¶ms); + Ok(SDiffParseOk::Params(params)) + } + + pub fn set_tabsize(&mut self, parsed_option: &ParsedOption) -> Result { + let tab_size = parsed_option.arg_for_option.clone().unwrap_or_default(); + let t = match tab_size.parse::() { + Ok(w) => w, + Err(_) => return Err(ParseError::InvalidValueNumber(parsed_option.clone())), + }; + self.tabsize = t; + + Ok(t) + } + + pub fn set_width(&mut self, parsed_option: &ParsedOption) -> Result { + let width = parsed_option.arg_for_option.clone().unwrap_or_default(); + let w = match width.parse::() { + Ok(w) => w, + Err(_) => return Err(ParseError::InvalidValueNumber(parsed_option.clone())), + }; + self.width = w; + + Ok(w) + } +} + +// Usually assert is used like assert_eq(test result, expected result). +#[cfg(test)] +mod tests { + use super::*; + + fn os(s: &str) -> OsString { + OsString::from(s) + } + + /// Simplify call of parser, just pass a normal string like in the Terminal. + fn parse(args: &str) -> ResultSDiffParse { + let mut o = Vec::new(); + for arg in args.split(' ') { + o.push(os(arg)); + } + let mut p = o.into_iter().peekable(); + // remove executable + let executable = Executable::from_args_os(&mut p, true).unwrap(); + + ParamsSDiff::parse_params(&executable, p) + } + + fn res_ok(params: ParamsSDiff) -> ResultSDiffParse { + Ok(SDiffParseOk::Params(params)) + } + + #[test] + fn positional() { + // file_1 and file_2 given + assert_eq!( + parse("sdiff foo bar"), + res_ok(ParamsSDiff { + executable: Executable::SDiff, + from: os("foo"), + to: os("bar"), + ..Default::default() + }), + ); + + // file_1 only + assert_eq!( + parse("sdiff foo"), + res_ok(ParamsSDiff { + executable: Executable::SDiff, + from: os("foo"), + to: os("-"), + ..Default::default() + }), + ); + + // double dash without operand + assert_eq!( + parse("sdiff foo -- --help"), + res_ok(ParamsSDiff { + executable: Executable::SDiff, + from: os("foo"), + to: os("--help"), + ..Default::default() + }), + ); + + // Err: no arguments + let msg = "missing operand after 'sdiff'"; + match parse("sdiff") { + Ok(_) => assert!(false, "Should not be ok!"), + Err(e) => assert!( + e.to_string().contains(msg), + "error must contain: \"{msg}\"\nactual error: \"{e}\"" + ), + } + + // Err: too many operands + let msg = "extra operand 'should-not-be-here'"; + match parse("sdiff foo bar should-not-be-here") { + Ok(_) => assert!(false, "Should not be ok!"), + Err(e) => assert!( + e.to_string().contains(msg), + "error must contain: \"{msg}\"\nactual error: \"{e}\"" + ), + } + } + + #[test] + fn execution_modes() { + // Test all options + // Disable feature "feat_check_not_yet_implemented" + // I^A is at the end of the single options, forcing '^A' as argument for 'I'. + // --wi is abbreviated and uses equal sign + // diff-program uses next arg + // -O uses next arg + let params = ParamsSDiff { + executable: Executable::SDiff, + from: os("foo"), + to: os("bar"), + diff_program: Some("prg".to_string()), + expand_tabs: true, + help: false, + ignore_all_space: true, + ignore_blank_lines: true, + ignore_case: true, + ignore_matching_lines: Some("^A".to_string()), + ignore_space_change: true, + ignore_tab_expansion: true, + ignore_trailing_space: true, + left_column: true, + minimal: true, + output: Some("out".to_string()), + speed_large_files: true, + strip_trailing_cr: true, + suppress_common_lines: true, + tabsize: 2, + text: true, + version: false, + width: 150, + }; + let r = parse( + "sdiff foo bar -iEZbWBalstdHI^A --wi=150 --diff-program prg -o out --strip --tab=2", + ); + match &r { + Ok(_) => assert_eq!(r, res_ok(params.clone())), + Err(e) => match e { + ParseError::NotYetImplemented(_) => {} + _ => assert_eq!(r, res_ok(params.clone())), + }, + } + + // negative value + // let msg = "invalid argument '-2' for '--tabsize'"; + let msg = "invalid --tabsize value '-2'"; + let r = parse("sdiff foo bar --tab=-2"); + match r { + Ok(_) => assert!(false, "Should not be Ok."), + Err(e) => assert!( + e.to_string().contains(msg), + "Must contain: {msg}\nactual: {e}" + ), + } + } +} diff --git a/src/side_diff.rs b/src/side_diff.rs index 56953d2..8d4bc7a 100644 --- a/src/side_diff.rs +++ b/src/side_diff.rs @@ -8,7 +8,7 @@ use diff::Result; use std::{io::Write, vec}; use unicode_width::UnicodeWidthStr; -use crate::params::Params; +use crate::sdiff::params_sdiff::ParamsSDiff; const GUTTER_WIDTH_MIN: usize = 3; @@ -98,6 +98,34 @@ impl Config { } } +/// Params for side_diff, so the functions can be used by multiple modules (diff and sdiff) +#[derive(Default)] +pub struct Params { + pub expand_tabs: bool, + pub tabsize: usize, + pub width: usize, +} + +impl From<&crate::params::Params> for Params { + fn from(param: &crate::params::Params) -> Self { + Self { + expand_tabs: param.expand_tabs, + tabsize: param.tabsize, + width: param.width, + } + } +} + +impl From<&ParamsSDiff> for Params { + fn from(param: &ParamsSDiff) -> Self { + Self { + expand_tabs: param.expand_tabs, + tabsize: param.tabsize, + width: param.width, + } + } +} + fn format_tabs_and_spaces( from: usize, to: usize, @@ -1093,7 +1121,7 @@ mod tests { let mut output = vec![]; diff(from_file, to_file, &mut output, ¶ms); - assert_eq!(output, vec![]); + assert_eq!(output, Vec::::new()); } #[test] diff --git a/src/utils.rs b/src/utils.rs index daca18d..325c4f1 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -4,7 +4,10 @@ // files that was distributed with this source code. use regex::Regex; -use std::{ffi::OsString, io::Write}; +use std::{ + ffi::{OsStr, OsString}, + io::Write, +}; use unicode_width::UnicodeWidthStr; /// Replace tabs by spaces in the input line. @@ -13,11 +16,11 @@ use unicode_width::UnicodeWidthStr; #[must_use] pub fn do_expand_tabs(line: &[u8], tabsize: usize) -> Vec { let tab = b'\t'; - let ntabs = line.iter().filter(|c| **c == tab).count(); - if ntabs == 0 { + let n_tabs = line.iter().filter(|c| **c == tab).count(); + if n_tabs == 0 { return line.to_vec(); } - let mut result = Vec::with_capacity(line.len() + ntabs * (tabsize - 1)); + let mut result = Vec::with_capacity(line.len() + n_tabs * (tabsize - 1)); let mut offset = 0; let mut iter = line.split(|c| *c == tab).peekable(); @@ -71,6 +74,11 @@ pub fn get_modification_time(file_path: &str) -> String { modification_time } +/// Checks if files are the same (same file link), which must return 'equal'. +pub fn is_same_file(from: &OsStr, to: &OsStr) -> bool { + (from == "-" && to == "-") || same_file::is_same_file(from, to).unwrap_or(false) +} + pub fn format_failure_to_read_input_file( executable: &OsString, filepath: &OsString,