diff --git a/Cargo.lock b/Cargo.lock index c8adbe7..57e6b13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,25 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "abnf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "087113bd50d9adce24850eed5d0476c7d199d532fce8fab5173650331e09033a" +dependencies = [ + "abnf-core", + "nom 7.1.3", +] + +[[package]] +name = "abnf-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c44e09c43ae1c368fb91a03a566472d0087c26cf7e1b9e8e289c14ede681dd7d" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "addr2line" version = "0.25.1" @@ -106,6 +125,39 @@ version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "btree-range-map" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1be5c9672446d3800bcbcaabaeba121fe22f1fb25700c4562b22faf76d377c33" +dependencies = [ + "btree-slab", + "cc-traits", + "range-traits", + "serde", + "slab", +] + +[[package]] +name = "btree-slab" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2b56d3029f075c4fa892428a098425b86cef5c89ae54073137ece416aef13c" +dependencies = [ + "cc-traits", + "slab", + "smallvec", +] + [[package]] name = "bumpalo" version = "3.19.0" @@ -122,12 +174,48 @@ dependencies = [ "shlex", ] +[[package]] +name = "cc-traits" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "060303ef31ef4a522737e1b1ab68c67916f2a787bb2f4f54f383279adba962b5" +dependencies = [ + "slab", +] + [[package]] name = "cfg-if" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "clap" version = "4.5.48" @@ -159,7 +247,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -174,6 +262,61 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "deranged" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[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.106", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -196,6 +339,25 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "gimli" version = "0.32.3" @@ -208,6 +370,16 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "half" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.16.0" @@ -220,6 +392,119 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex_fmt" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "include_dir" version = "0.7.4" @@ -249,6 +534,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "indoc" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" + [[package]] name = "is_ci" version = "1.2.0" @@ -277,6 +568,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "langtag" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecb4c689a30e48ebeaa14237f34037e300dd072e6ad21a9ec72e810ff3c6600" +dependencies = [ + "static-regular-grammar", + "thiserror 1.0.69", +] + [[package]] name = "libc" version = "0.2.176" @@ -289,6 +590,12 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + [[package]] name = "log" version = "0.4.28" @@ -328,7 +635,7 @@ checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -341,6 +648,12 @@ dependencies = [ "walkdir", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -363,7 +676,7 @@ dependencies = [ "mlf-validation", "serde", "serde_json", - "thiserror", + "thiserror 2.0.17", ] [[package]] @@ -387,15 +700,20 @@ name = "mlf-lang" version = "0.1.0" dependencies = [ "include_dir", - "nom", + "nom 8.0.0", ] [[package]] name = "mlf-validation" version = "0.1.0" dependencies = [ + "langtag", "mlf-lang", + "regex", "serde_json", + "time", + "unicode-segmentation", + "url", ] [[package]] @@ -412,6 +730,16 @@ dependencies = [ "wasm-bindgen-test", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nom" version = "8.0.0" @@ -421,6 +749,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "object" version = "0.37.3" @@ -448,6 +782,51 @@ version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.101" @@ -466,6 +845,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "range-traits" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d20581732dd76fa913c7dff1a2412b714afe3573e94d41c34719de73337cc8ab" + [[package]] name = "regex" version = "1.11.3" @@ -573,7 +958,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -590,12 +975,61 @@ dependencies = [ "serde_core", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static-regular-grammar" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4a6c40247579acfbb138c3cd7de3dab113ab4ac6227f1b7de7d626ee667957" +dependencies = [ + "abnf", + "btree-range-map", + "ciborium", + "hex_fmt", + "indoc", + "proc-macro-error", + "proc-macro2", + "quote", + "serde", + "sha2", + "syn 2.0.106", + "thiserror 1.0.69", +] + [[package]] name = "strsim" version = "0.11.1" @@ -623,6 +1057,16 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.106" @@ -634,6 +1078,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "terminal_size" version = "0.4.3" @@ -654,13 +1109,33 @@ dependencies = [ "unicode-width 0.2.1", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] @@ -671,7 +1146,47 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", ] [[package]] @@ -692,6 +1207,12 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + [[package]] name = "unicode-ident" version = "1.0.19" @@ -704,6 +1225,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "unicode-width" version = "0.1.14" @@ -716,12 +1243,36 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "walkdir" version = "2.5.0" @@ -755,7 +1306,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn", + "syn 2.0.106", "wasm-bindgen-shared", ] @@ -790,7 +1341,7 @@ checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -825,7 +1376,7 @@ checksum = "b673bca3298fe582aeef8352330ecbad91849f85090805582400850f8270a2e8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -926,3 +1477,87 @@ name = "windows_x86_64_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] diff --git a/mlf-validation/Cargo.toml b/mlf-validation/Cargo.toml index 3db9929..26754c8 100644 --- a/mlf-validation/Cargo.toml +++ b/mlf-validation/Cargo.toml @@ -7,3 +7,8 @@ license = "MIT" [dependencies] mlf-lang = { path = "../mlf-lang" } serde_json = "1" +unicode-segmentation = "1.12" +regex = { version = "1", default-features = false, features = ["std", "unicode-perl"] } +url = "2" +time = { version = "0.3", features = ["parsing", "macros"] } +langtag = "0.4" diff --git a/mlf-validation/src/lib.rs b/mlf-validation/src/lib.rs index 0585a2b..9263fbc 100644 --- a/mlf-validation/src/lib.rs +++ b/mlf-validation/src/lib.rs @@ -1,6 +1,12 @@ use mlf_lang::ast::*; use serde_json::Value as JsonValue; use std::fmt; +use unicode_segmentation::UnicodeSegmentation; +use regex::Regex; +use url::Url; +use time::format_description::well_known::Rfc3339; +use time::OffsetDateTime; +use langtag::LangTag; #[derive(Debug, Clone)] pub struct ValidationError { @@ -243,6 +249,14 @@ impl<'a> RecordValidator<'a> { message: format!("String too short: {} bytes (min: {})", s.len(), min), }); } + } else if let Some(arr) = value.as_array() { + // MinLength can also apply to arrays (element count) + if arr.len() < *min { + errors.push(ValidationError { + path: path.to_string(), + message: format!("Array too short: {} elements (min: {})", arr.len(), min), + }); + } } } Constraint::MaxLength { value: max, .. } => { @@ -253,11 +267,20 @@ impl<'a> RecordValidator<'a> { message: format!("String too long: {} bytes (max: {})", s.len(), max), }); } + } else if let Some(arr) = value.as_array() { + // MaxLength can also apply to arrays (element count) + if arr.len() > *max { + errors.push(ValidationError { + path: path.to_string(), + message: format!("Array too long: {} elements (max: {})", arr.len(), max), + }); + } } } Constraint::MinGraphemes { value: min, .. } => { if let Some(s) = value.as_str() { - let count = s.chars().count(); // Simplified grapheme count + // Use proper Unicode grapheme cluster counting + let count = s.graphemes(true).count(); if count < *min { errors.push(ValidationError { path: path.to_string(), @@ -268,7 +291,8 @@ impl<'a> RecordValidator<'a> { } Constraint::MaxGraphemes { value: max, .. } => { if let Some(s) = value.as_str() { - let count = s.chars().count(); // Simplified grapheme count + // Use proper Unicode grapheme cluster counting + let count = s.graphemes(true).count(); if count > *max { errors.push(ValidationError { path: path.to_string(), @@ -362,115 +386,26 @@ impl<'a> RecordValidator<'a> { path: &str, errors: &mut Vec, ) { - // TODO: Implement proper format validation according to ATProto spec - // These are currently basic checks and should be replaced with proper validation: - // - datetime: RFC 3339 datetime validation - // - uri: RFC 3986 URI validation - // - at-uri: ATProto AT-URI validation - // - did: DID spec validation (did:plc:, did:web:, etc.) - // - handle: ATProto handle validation - // - nsid: NSID validation (reverse domain name) - // - cid: CID validation (multibase-encoded multihash) - // - at-identifier: either a DID or handle - // - language: BCP 47 language tag validation - // - tid: TID validation (base32-sortable timestamp) - // - record-key: Record key validation - - match format { - "datetime" => { - // Basic ISO 8601 datetime validation - if !value.contains('T') && !value.contains('Z') && !value.contains('+') && !value.contains('-') { - errors.push(ValidationError { - path: path.to_string(), - message: format!("Invalid datetime format: '{}' (expected ISO 8601)", value), - }); - } - } - "uri" => { - if !value.starts_with("http://") && !value.starts_with("https://") && !value.starts_with("ftp://") { - errors.push(ValidationError { - path: path.to_string(), - message: format!("Invalid URI: '{}' (expected http://, https://, or ftp://)", value), - }); - } - } - "at-uri" => { - if !value.starts_with("at://") { - errors.push(ValidationError { - path: path.to_string(), - message: format!("Invalid AT-URI: '{}' (expected at://)", value), - }); - } - } - "did" => { - if !value.starts_with("did:") { - errors.push(ValidationError { - path: path.to_string(), - message: format!("Invalid DID: '{}' (expected did:)", value), - }); - } - } - "handle" => { - // Basic handle validation (domain-like) - if !value.contains('.') || value.starts_with('.') || value.ends_with('.') { - errors.push(ValidationError { - path: path.to_string(), - message: format!("Invalid handle: '{}' (expected domain format)", value), - }); - } - } - "nsid" => { - // NSID format: authority.name(.name)* - let parts: Vec<&str> = value.split('.').collect(); - if parts.len() < 2 { - errors.push(ValidationError { - path: path.to_string(), - message: format!("Invalid NSID: '{}' (expected at least 2 segments)", value), - }); - } - } - "cid" => { - // CID validation is complex, just check not empty - if value.is_empty() { - errors.push(ValidationError { - path: path.to_string(), - message: "Invalid CID: empty string".to_string(), - }); - } - } - "at-identifier" => { - // Can be either DID or handle - let is_did = value.starts_with("did:"); - let is_handle = value.contains('.') && !value.starts_with('.') && !value.ends_with('.'); + let is_valid = match format { + "datetime" => validate_datetime(value), + "uri" => validate_uri(value), + "at-uri" => validate_at_uri(value), + "did" => validate_did(value), + "handle" => validate_handle(value), + "nsid" => validate_nsid(value), + "cid" => validate_cid(value), + "at-identifier" => validate_at_identifier(value), + "language" => validate_language(value), + "tid" => validate_tid(value), + "record-key" => validate_record_key(value), + _ => true, // Unknown format, pass validation + }; - if !is_did && !is_handle { - errors.push(ValidationError { - path: path.to_string(), - message: format!("Invalid AT-identifier: '{}' (expected DID or handle)", value), - }); - } - } - "language" => { - // Basic BCP 47 validation (simplified) - if value.len() < 2 || !value.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') { - errors.push(ValidationError { - path: path.to_string(), - message: format!("Invalid language code: '{}'", value), - }); - } - } - "tid" | "record-key" => { - // Basic validation for TID and record-key - if value.is_empty() { - errors.push(ValidationError { - path: path.to_string(), - message: format!("Invalid {}: empty string", format), - }); - } - } - _ => { - // Unknown format, skip validation - } + if !is_valid { + errors.push(ValidationError { + path: path.to_string(), + message: format!("Invalid {} format: '{}'", format, value), + }); } } @@ -569,3 +504,380 @@ fn value_type_name(value: &JsonValue) -> &'static str { JsonValue::Object(_) => "object", } } + +// Format validators + +/// Validate datetime format (RFC 3339 / ISO 8601) +fn validate_datetime(value: &str) -> bool { + // Use time crate for proper RFC 3339 parsing + OffsetDateTime::parse(value, &Rfc3339).is_ok() +} + +/// Validate URI format (RFC 3986) +fn validate_uri(value: &str) -> bool { + // Use url crate for proper URI parsing + Url::parse(value).is_ok() +} + +/// Validate AT-URI format (at://did:plc:xyz/com.example.foo/record-key) +fn validate_at_uri(value: &str) -> bool { + // AT-URI format: at://authority/collection/rkey + // authority is a DID or handle + // collection is an NSID + // rkey is optional + + if !value.starts_with("at://") { + return false; + } + + // Strip the scheme + let without_scheme = &value[5..]; + + // Split by first slash to get authority and path + let (authority, path) = match without_scheme.split_once('/') { + Some((auth, p)) => (auth, Some(p)), + None => (without_scheme, None), + }; + + // Authority must be a DID or handle + if !validate_did(authority) && !validate_handle(authority) { + return false; + } + + // Path validation (if present) + if let Some(path_str) = path { + if !path_str.is_empty() { + // Path should be collection or collection/rkey + let parts: Vec<&str> = path_str.split('/').filter(|s| !s.is_empty()).collect(); + if parts.is_empty() || parts.len() > 2 { + return false; + } + // Collection should be an NSID + if !validate_nsid(parts[0]) { + return false; + } + // Record key validation (if present) + if parts.len() == 2 && !validate_record_key(parts[1]) { + return false; + } + } + } + + true +} + +/// Validate DID format (did:method:identifier) +fn validate_did(value: &str) -> bool { + // DID format: did:method:method-specific-id + // method: lowercase letters, numbers + // method-specific-id: alphanumeric plus . - _ : + let re = Regex::new( + r"^did:[a-z0-9]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$" + ).unwrap(); + re.is_match(value) +} + +/// Validate handle format (domain name) +fn validate_handle(value: &str) -> bool { + // Handle is a domain name: segment.segment.segment + // Each segment: alphanumeric and hyphen, can't start or end with hyphen + // Must have at least one dot + if !value.contains('.') || value.starts_with('.') || value.ends_with('.') { + return false; + } + + // Check each segment + for segment in value.split('.') { + if segment.is_empty() + || segment.starts_with('-') + || segment.ends_with('-') + || segment.len() > 63 { + return false; + } + if !segment.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') { + return false; + } + } + + // Total length check + value.len() <= 253 +} + +/// Validate NSID format (namespaced identifier) +fn validate_nsid(value: &str) -> bool { + // NSID format: authority.name(.name)* + // authority: domain name (reversed) + // name: lowercase alphanumeric, max 63 chars per segment + // Total: 3-317 chars + if value.len() < 3 || value.len() > 317 { + return false; + } + + let parts: Vec<&str> = value.split('.').collect(); + if parts.len() < 3 { + return false; + } + + // Check each segment + for part in &parts { + if part.is_empty() || part.len() > 63 { + return false; + } + // NSID segments must be lowercase alphanumeric (and hyphen for domain parts) + if !part.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') { + return false; + } + // Can't start with digit + if part.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false) { + return false; + } + } + + true +} + +/// Validate CID format (Content Identifier) +fn validate_cid(value: &str) -> bool { + // CID format is complex (multibase encoded multihash) + // Basic validation: non-empty, starts with base58btc or base32 prefix + // Full validation would require parsing the multibase/multihash + if value.is_empty() { + return false; + } + + // CIDv0: starts with 'Qm' (base58btc) + // CIDv1: starts with 'b' (base32) or 'z' (base58btc) followed by version + if value.starts_with("Qm") && value.len() == 46 { + // CIDv0 - all base58btc chars + return value.chars().all(|c| { + c.is_ascii_alphanumeric() && c != '0' && c != 'O' && c != 'I' && c != 'l' + }); + } + + if (value.starts_with('b') || value.starts_with('z')) && value.len() > 10 { + // CIDv1 - basic check for valid multibase chars + return value.chars().all(|c| c.is_ascii_alphanumeric()); + } + + false +} + +/// Validate AT-identifier format (DID or handle) +fn validate_at_identifier(value: &str) -> bool { + validate_did(value) || validate_handle(value) +} + +/// Validate language code format (BCP 47) +fn validate_language(value: &str) -> bool { + // Use langtag crate for proper BCP 47 / RFC 5646 validation + LangTag::new(value).is_ok() +} + +/// Validate TID format (Timestamp Identifier) +fn validate_tid(value: &str) -> bool { + // TID: 13 character base32-sortable timestamp + // Uses a-z2-7 character set (no 0,1,8,9) + if value.len() != 13 { + return false; + } + + value.chars().all(|c| { + matches!(c, 'a'..='z' | '2'..='7') + }) +} + +/// Validate record-key format +fn validate_record_key(value: &str) -> bool { + // Record key: alphanumeric, dot, underscore, tilde, hyphen + // 1-512 characters + // Can be TID or custom key + if value.is_empty() || value.len() > 512 { + return false; + } + + // If it looks like a TID, validate as TID + if value.len() == 13 && value.chars().all(|c| matches!(c, 'a'..='z' | '2'..='7')) { + return validate_tid(value); + } + + // Otherwise, general record key validation + value.chars().all(|c| { + c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '~' || c == '-' + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_validate_datetime() { + // Valid datetimes + assert!(validate_datetime("2024-01-15T10:30:00Z")); + assert!(validate_datetime("2024-01-15T10:30:00.123Z")); + assert!(validate_datetime("2024-01-15T10:30:00+05:30")); + assert!(validate_datetime("2024-01-15T10:30:00-08:00")); + + // Invalid datetimes + assert!(!validate_datetime("2024-01-15")); + assert!(!validate_datetime("2024-01-15 10:30:00")); + assert!(!validate_datetime("not-a-date")); + } + + #[test] + fn test_validate_uri() { + // Valid URIs + assert!(validate_uri("https://example.com")); + assert!(validate_uri("http://example.com/path")); + assert!(validate_uri("ftp://example.com")); + assert!(validate_uri("custom-scheme://something")); + + // Invalid URIs + assert!(!validate_uri("not a uri")); + assert!(!validate_uri("://missing-scheme")); + assert!(!validate_uri("")); + } + + #[test] + fn test_validate_at_uri() { + // Valid AT-URIs + assert!(validate_at_uri("at://did:plc:abc123")); + assert!(validate_at_uri("at://did:plc:abc123/com.example.foo")); + assert!(validate_at_uri("at://did:plc:abc123/com.example.foo/abc123")); + assert!(validate_at_uri("at://alice.example.com/com.example.post/abc")); + + // Invalid AT-URIs + assert!(!validate_at_uri("https://example.com")); + assert!(!validate_at_uri("at://")); + assert!(!validate_at_uri("not-at-uri")); + } + + #[test] + fn test_validate_did() { + // Valid DIDs + assert!(validate_did("did:plc:abc123xyz")); + assert!(validate_did("did:web:example.com")); + assert!(validate_did("did:key:abc123")); + + // Invalid DIDs + assert!(!validate_did("not-a-did")); + assert!(!validate_did("did:")); + assert!(!validate_did("did:UPPERCASE:test")); // method must be lowercase + } + + #[test] + fn test_validate_handle() { + // Valid handles + assert!(validate_handle("example.com")); + assert!(validate_handle("alice.example.com")); + assert!(validate_handle("my-site.example.com")); + + // Invalid handles + assert!(!validate_handle("nodomainext")); + assert!(!validate_handle(".example.com")); + assert!(!validate_handle("example.com.")); + assert!(!validate_handle("-invalid.com")); + assert!(!validate_handle("invalid-.com")); + } + + #[test] + fn test_validate_nsid() { + // Valid NSIDs + assert!(validate_nsid("com.example.foo")); + assert!(validate_nsid("com.example.foo.bar")); + assert!(validate_nsid("io.github.user.action")); + + // Invalid NSIDs + assert!(!validate_nsid("com.example")); // need at least 3 segments + assert!(!validate_nsid("COM.EXAMPLE.FOO")); // must be lowercase + assert!(!validate_nsid("com.123invalid.foo")); // can't start with digit + assert!(!validate_nsid("co")); // too short + } + + #[test] + fn test_validate_cid() { + // Valid CIDs (examples) + assert!(validate_cid("QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG")); // CIDv0 + assert!(validate_cid("bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku")); // CIDv1 + + // Invalid CIDs + assert!(!validate_cid("")); + assert!(!validate_cid("not-a-cid")); + assert!(!validate_cid("Qm123")); // too short + } + + #[test] + fn test_validate_at_identifier() { + // Valid (DIDs) + assert!(validate_at_identifier("did:plc:abc123")); + + // Valid (handles) + assert!(validate_at_identifier("example.com")); + assert!(validate_at_identifier("alice.example.com")); + + // Invalid + assert!(!validate_at_identifier("not-valid")); + assert!(!validate_at_identifier("")); + } + + #[test] + fn test_validate_language() { + // Valid language codes (BCP 47 / RFC 5646) + assert!(validate_language("en")); + assert!(validate_language("en-US")); + assert!(validate_language("zh-Hans-CN")); + assert!(validate_language("fr-CA")); + assert!(validate_language("en-GB")); + assert!(validate_language("de-DE")); + + // Invalid language codes + assert!(!validate_language("e")); // too short + assert!(!validate_language("en_US")); // wrong separator (underscore) + assert!(!validate_language("")); // empty + assert!(!validate_language("123")); // starts with digit + assert!(!validate_language("en--US")); // double separator + } + + #[test] + fn test_validate_tid() { + // Valid TIDs (13 chars, base32-sortable) + assert!(validate_tid("3jui7kd54zh2y")); + assert!(validate_tid("3k2a4dqudbbz2")); + + // Invalid TIDs + assert!(!validate_tid("3jui7kd54zh2")); // too short + assert!(!validate_tid("3jui7kd54zh2yy")); // too long + assert!(!validate_tid("3jui7kd54zh2Y")); // uppercase not allowed + assert!(!validate_tid("3jui0kd54zh2y")); // 0 not allowed + } + + #[test] + fn test_validate_record_key() { + // Valid record keys + assert!(validate_record_key("3jui7kd54zh2y")); // TID + assert!(validate_record_key("my-record-key")); + assert!(validate_record_key("key.with.dots")); + assert!(validate_record_key("key_with_underscores")); + assert!(validate_record_key("key~with~tildes")); + + // Invalid record keys + assert!(!validate_record_key("")); // empty + assert!(!validate_record_key(&"a".repeat(513))); // too long + assert!(!validate_record_key("key with spaces")); // spaces not allowed + } + + #[test] + fn test_grapheme_counting() { + // Unicode grapheme cluster counting test + use unicode_segmentation::UnicodeSegmentation; + + let text = "👨‍👩‍👧‍👦"; // Family emoji (1 grapheme cluster) + assert_eq!(text.graphemes(true).count(), 1); + + let text = "hello"; // 5 graphemes + assert_eq!(text.graphemes(true).count(), 5); + + let text = "नमस्ते"; // Devanagari (3 grapheme clusters) + assert_eq!(text.graphemes(true).count(), 3); + } +}