diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..de600e5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,53 @@ +# Changelog + +## 0.2.0 + +This release is a rewrite of the parser to follow the WHATWG Fetch Standard (what browsers do), +verified against the Web Platform Tests suites. It fixes several bugs that made 0.1.x unusable +for most real-world data URLs, and cleans up the API, which is why it's a breaking release. + +### Fixed + + - Base64 now uses the standard alphabet (`+` and `/`). 0.1.x used the URL-safe one (`-` and `_`), + so it failed to parse most base64 data URLs, and generated ones browsers couldn't decode + - Base64 decoding is now forgiving like in browsers: whitespace is ignored and padding is optional + - Serializing no longer transcodes data: bytes that weren't valid UTF-8 (e.g. windows-1252 text) + used to get replaced with U+FFFD + - `charset` is now recognized after whitespace (`data:text/html; charset=utf-8,…`) and when quoted + - Media type parameters other than `charset` are no longer dropped + - Inputs with more than 127 parameters no longer cause an integer overflow + - The CLI no longer ignores `-o` when encoding, and no longer refuses `-o` when STDOUT is redirected + - Writing into a closed pipe (e.g. `dataurl -d … | head`) no longer panics + +### Changed + + - `DataUrlParseError` is replaced by `Error`, which has more precise variants, + and implements `Display` and `std::error::Error` + - `to_string()` now comes from `Display`, so `format!("{data_url}")` works too + - Getters return borrowed values: `media_type_no_default()`, `charset_no_default()` + and `fragment()` return `Option<&str>`, and `text()` returns `Cow` + - Setters take `Option<&str>`; `set_media_type()` and `set_charset()` return `Result<(), Error>` + instead of `bool` + - `set_media_type()` only accepts `type/subtype`; use `set_parameter()` for parameters + - Percent-encoded output is much more readable, only escaping what URLs, HTML attributes and CSS require + (`data:,Hello,%20world!` instead of `data:,Hello%2C%20world%21`) + - When given text as an argument together with `-c`, the CLI fails if the text can't be represented + in that charset (it used to output HTML numeric character references like `Ü`) + - CLI error messages are more descriptive, and usage errors exit with code 2 + - Updated to Rust 2021 edition, minimum supported Rust version is 1.73 + +### Added + + - `FromStr` and `TryFrom<&str>`, so `"data:,…".parse::()` works + - `Debug`, `Clone`, `PartialEq`, `Eq` and `Default` for `DataUrl` (`Debug` doesn't print the data, only its length) + - `mime_type()`, the full MIME type the way browsers report it (e.g. `text/plain;charset=US-ASCII`) + - `parameters()`, `parameter()` and `set_parameter()` for media type parameters + - `encoding()`, the `encoding_rs` encoding used for text + - `set_text()`, which encodes text in the data URL's charset + - `into_data()` + - Support for `--option=value` and `-ovalue` forms in the CLI + +### Removed + + - Dependencies on `atty` (unmaintained, RUSTSEC-2021-0145), `clap`, `base64`, `percent-encoding` and `url`; + the CLI and the library now only depend on `encoding_rs` diff --git a/Cargo.lock b/Cargo.lock index 18ac308..b16a177 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,601 +2,59 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - -[[package]] -name = "anstyle" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" - -[[package]] -name = "assert_cmd" -version = "2.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" -dependencies = [ - "anstyle", - "bstr", - "doc-comment", - "libc", - "predicates", - "predicates-core", - "predicates-tree", - "wait-timeout", -] - -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bstr" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8" -dependencies = [ - "memchr", - "regex-automata", - "serde", -] - [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" [[package]] -name = "clap" -version = "2.34.0" +name = "core_detect" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" -dependencies = [ - "ansi_term", - "atty", - "bitflags", - "strsim", - "textwrap", - "unicode-width", - "vec_map", -] +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" [[package]] name = "dataurl" -version = "0.1.2" +version = "0.2.0" dependencies = [ - "assert_cmd", - "atty", - "base64", - "clap", "encoding_rs", - "percent-encoding", - "url", -] - -[[package]] -name = "difflib" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", ] -[[package]] -name = "doc-comment" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" - [[package]] name = "encoding_rs" -version = "0.8.35" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" dependencies = [ "cfg-if", + "core_detect", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", ] [[package]] -name = "form_urlencoded" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "icu_collections" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locid" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - -[[package]] -name = "icu_normalizer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "utf16_iter", - "utf8_iter", - "write16", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" - -[[package]] -name = "icu_properties" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locid_transform", - "icu_properties_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" - -[[package]] -name = "icu_provider" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "idna" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "libc" -version = "0.2.169" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" - -[[package]] -name = "litemap" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "percent-encoding" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" - -[[package]] -name = "predicates" -version = "3.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" -dependencies = [ - "anstyle", - "difflib", - "predicates-core", -] - -[[package]] -name = "predicates-core" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" - -[[package]] -name = "predicates-tree" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" -dependencies = [ - "predicates-core", - "termtree", -] - -[[package]] -name = "proc-macro2" -version = "1.0.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" - -[[package]] -name = "serde" -version = "1.0.216" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.216" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "smallvec" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" - -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - -[[package]] -name = "strsim" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - -[[package]] -name = "syn" -version = "2.0.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "synstructure" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "termtree" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" - -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] - -[[package]] -name = "tinystr" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "unicode-ident" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" - -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "url" -version = "2.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] - -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - -[[package]] -name = "wait-timeout" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" -dependencies = [ - "libc", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[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-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "write16" +name = "multiversion_no_op" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - -[[package]] -name = "writeable" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" - -[[package]] -name = "yoke" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" [[package]] -name = "yoke-derive" -version = "0.7.5" +name = "rustversion" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] -name = "zerofrom" -version = "0.1.5" +name = "scopeguard" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" -dependencies = [ - "zerofrom-derive", -] +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] -name = "zerofrom-derive" +name = "simdutf8" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerovec" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" diff --git a/Cargo.toml b/Cargo.toml index abe4644..d1857e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,37 +1,36 @@ [package] name = "dataurl" -version = "0.1.2" +version = "0.2.0" authors = [ - "Sunshine ", + "Sunshine ", ] -edition = "2018" +edition = "2021" +rust-version = "1.73" description = "CLI tool and Rust crate for parsing and generating data URLs" homepage = "https://github.com/Y2Z/dataurl" repository = "https://github.com/Y2Z/dataurl" +documentation = "https://docs.rs/dataurl" readme = "README.md" -keywords = ["web", "command-line"] +keywords = ["data-url", "base64", "mime", "uri", "command-line"] categories = ["command-line-utilities", "encoding", "parsing", "web-programming"] include = [ - "src/*.rs", + "src/**/*.rs", + "tests/**/*", "Cargo.toml", + "CHANGELOG.md", + "LICENSE", + "README.md", ] license = "CC0-1.0" [dependencies] -atty = "0.2.14" -base64 = "0.22.1" -clap = { version = "2.33.3", optional = true } -encoding_rs = "0.8.35" -percent-encoding = "2.3.1" -url = "2.5.4" - -[dev-dependencies] -assert_cmd = "2.0.16" +encoding_rs = "0.8.35" # Used for charsets (label validation, decoding and encoding text) [features] default = ["cli"] -cli = ["clap"] +cli = [] # Build the dataurl executable (no extra dependencies, kept for compatibility) [[bin]] name = "dataurl" +path = "src/main.rs" required-features = ["cli"] diff --git a/README.md b/README.md index 0c2f623..de2c904 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,9 @@ CLI tool / Rust crate for converting files and text into data URLs and back +Parsing follows the [WHATWG Fetch Standard](https://fetch.spec.whatwg.org/#data-url-processor), which is what browsers implement, and is checked against the [Web Platform Tests](https://github.com/web-platform-tests/wpt) suites for data URLs, base64, and MIME types. +The only dependency is [encoding_rs](https://crates.io/crates/encoding_rs), used for charsets. + --------------------------------------------------- @@ -21,7 +24,7 @@ dataurl -d 'data:text/html,textok?a=v#f' > index.html ``` ```console -dataurl -b -i picture.png +dataurl -b -t image/png -i picture.png ``` ```console @@ -29,13 +32,13 @@ cat file.txt | dataurl -i - -o - | dataurl -d ``` ```console -cat file.png | dataurl +cat file.png | dataurl -b -t image/png ``` ### Flags and options - `-b`: Encode data using base64 - - `-c`: Use custom `charset` + - `-c`: Use custom `charset` (text given as an argument gets converted to it) - `-d`: Attempt to parse input, output resulting data - `-f`: Append `fragment` - `-i`: Specify `file` to read data from (use `-` for STDIN) @@ -48,22 +51,40 @@ cat file.png | dataurl ## Usage (crate) +```toml +[dependencies] +dataurl = { version = "0.2", default-features = false } +``` + ```rust use dataurl::DataUrl; -let data_url: DataUrl = DataUrl::parse("data:,Hello,%20World!")?; +let data_url: DataUrl = "data:,Hello,%20World!".parse()?; -assert_eq!(data_url.media_type(), "text/plain".to_string()); +assert_eq!(data_url.media_type(), "text/plain"); assert_eq!(data_url.media_type_no_default(), None); -assert_eq!(data_url.charset(), "US-ASCII".to_string()); +assert_eq!(data_url.mime_type(), "text/plain;charset=US-ASCII"); +assert_eq!(data_url.charset(), "US-ASCII"); assert_eq!(data_url.charset_no_default(), None); assert!(!data_url.is_base64_encoded()); -assert_eq!(data_url.data(), [72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33]); +assert_eq!(data_url.data(), b"Hello, World!"); assert_eq!(data_url.fragment(), None); -assert_eq!(data_url.to_string(), "data:,Hello%2C%20World%21"); assert_eq!(data_url.text(), "Hello, World!"); +assert_eq!(data_url.to_string(), "data:,Hello,%20World!"); + +let mut data_url = DataUrl::new(); +data_url.set_media_type(Some("image/png"))?; +data_url.set_is_base64_encoded(true); +data_url.set_data(&[0x89, 0x50, 0x4E, 0x47]); +assert_eq!(data_url.to_string(), "data:image/png;base64,iVBORw=="); ``` +Data is stored as raw bytes and serialized as-is; use `set_text()` to store text encoded in the data URL's charset, and `text()` to decode it. +Disabling default features skips the `cli` feature, which only matters for `cargo install`. + +If all you need is parsing, [data-url](https://crates.io/crates/data-url) (from the Servo project) is another spec-compliant option; +dataurl also generates data URLs, and comes with a CLI tool. + --------------------------------------------------- @@ -93,7 +114,7 @@ make install #### Using [pre-built binaries](https://github.com/Y2Z/dataurl/releases) (Windows, ARM-based devices, etc) -Every release contains pre-built binaries for Windows, GNU/Linux, as well as platforms with non-standart CPU architecture. +Every release contains pre-built binaries for Windows, GNU/Linux, as well as platforms with non-standard CPU architecture. --------------------------------------------------- @@ -101,6 +122,9 @@ Every release contains pre-built binaries for Windows, GNU/Linux, as well as pla ## References + - [WHATWG Fetch Standard: data: URL processor](https://fetch.spec.whatwg.org/#data-url-processor) + - [WHATWG MIME Sniffing Standard: parsing a MIME type](https://mimesniff.spec.whatwg.org/#parsing-a-mime-type) + - [WHATWG Infra Standard: forgiving-base64 decode](https://infra.spec.whatwg.org/#forgiving-base64-decode) - [RFC 2397 (The "data" URL scheme)](https://datatracker.ietf.org/doc/html/rfc2397) - [RFC 6838 (Media Type Specifications and Registration Procedures)](https://datatracker.ietf.org/doc/html/rfc6838) @@ -112,3 +136,5 @@ Every release contains pre-built binaries for Windows, GNU/Linux, as well as pla To the extent possible under law, the author(s) have dedicated all copyright related and neighboring rights to this software to the public domain worldwide. This software is distributed without any warranty. + +The Web Platform Tests fixtures in `tests/wpt/` are distributed under their own [3-Clause BSD License](tests/wpt/README.md). diff --git a/src/base64.rs b/src/base64.rs new file mode 100644 index 0000000..31955cb --- /dev/null +++ b/src/base64.rs @@ -0,0 +1,123 @@ +//! Standard base64 (RFC 4648 §4, the alphabet with "+" and "/"), which is what data URLs use. + +const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +pub(crate) fn encode_into(output: &mut String, data: &[u8]) { + output.reserve(data.len().div_ceil(3) * 4); + + for chunk in data.chunks(3) { + let n = (chunk[0] as u32) << 16 + | (*chunk.get(1).unwrap_or(&0) as u32) << 8 + | *chunk.get(2).unwrap_or(&0) as u32; + + output.push(ALPHABET[(n >> 18) as usize & 63] as char); + output.push(ALPHABET[(n >> 12) as usize & 63] as char); + output.push(if chunk.len() > 1 { + ALPHABET[(n >> 6) as usize & 63] as char + } else { + '=' + }); + output.push(if chunk.len() > 2 { + ALPHABET[n as usize & 63] as char + } else { + '=' + }); + } +} + +fn decode_char(c: u8) -> Option { + match c { + b'A'..=b'Z' => Some((c - b'A') as u32), + b'a'..=b'z' => Some((c - b'a' + 26) as u32), + b'0'..=b'9' => Some((c - b'0' + 52) as u32), + b'+' => Some(62), + b'/' => Some(63), + _ => None, + } +} + +/// The "forgiving-base64 decode" algorithm from the WHATWG Infra Standard +/// (): ASCII whitespace is ignored, +/// padding is optional, and anything else outside the alphabet makes decoding fail. +pub(crate) fn decode_forgiving(input: &[u8]) -> Option> { + let mut data: Vec = input + .iter() + .copied() + .filter(|c| !matches!(c, b'\t' | b'\n' | 0x0C | b'\r' | b' ')) + .collect(); + + if data.len() % 4 == 0 { + if data.ends_with(b"==") { + data.truncate(data.len() - 2); + } else if data.ends_with(b"=") { + data.truncate(data.len() - 1); + } + } + + if data.len() % 4 == 1 { + return None; + } + + let mut output: Vec = Vec::with_capacity(data.len() / 4 * 3 + 2); + let mut buffer: u32 = 0; + let mut bits: u8 = 0; + + for c in data { + buffer = buffer << 6 | decode_char(c)?; + bits += 6; + if bits == 24 { + output.extend_from_slice(&[(buffer >> 16) as u8, (buffer >> 8) as u8, buffer as u8]); + buffer = 0; + bits = 0; + } + } + + // Leftover bits that don't form a whole byte get discarded + match bits { + 12 => output.push((buffer >> 4) as u8), + 18 => output.extend_from_slice(&[(buffer >> 10) as u8, (buffer >> 2) as u8]), + _ => {} + } + + Some(output) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn encodes_with_standard_alphabet_and_padding() { + let mut s = String::new(); + encode_into(&mut s, &[0xFB, 0xFF]); + assert_eq!(s, "+/8="); + + for (input, expected) in [ + ("", ""), + ("f", "Zg=="), + ("fo", "Zm8="), + ("foo", "Zm9v"), + ("foob", "Zm9vYg=="), + ("fooba", "Zm9vYmE="), + ("foobar", "Zm9vYmFy"), + ] { + let mut s = String::new(); + encode_into(&mut s, input.as_bytes()); + assert_eq!(s, expected); + assert_eq!(decode_forgiving(s.as_bytes()).unwrap(), input.as_bytes()); + } + } + + #[test] + fn round_trips_all_byte_values() { + let data: Vec = (0..=255).cycle().take(1000).collect(); + let mut s = String::new(); + encode_into(&mut s, &data); + assert_eq!(decode_forgiving(s.as_bytes()).unwrap(), data); + } + + #[test] + fn rejects_url_safe_alphabet() { + assert_eq!(decode_forgiving(b"-_8="), None); + } +} diff --git a/src/lib.rs b/src/lib.rs index 319ac35..5520a56 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,378 +1,559 @@ -use base64::{engine::general_purpose::URL_SAFE, Engine as _}; -use encoding_rs::Encoding; -use percent_encoding::{percent_decode_str, percent_encode, utf8_percent_encode, NON_ALPHANUMERIC}; +//! Parsing and generating `data:` URLs. +//! +//! Parsing follows the [data: URL processor] from the WHATWG Fetch Standard, which is what +//! browsers implement (RFC 2397 describes the scheme, but it's not what the web runs on). +//! The implementation is checked against the Web Platform Tests suites for data URLs, +//! forgiving base64, and MIME type parsing. +//! +//! ``` +//! use dataurl::DataUrl; +//! +//! let data_url: DataUrl = "data:,Hello,%20World!".parse()?; +//! assert_eq!(data_url.media_type(), "text/plain"); +//! assert_eq!(data_url.mime_type(), "text/plain;charset=US-ASCII"); +//! assert_eq!(data_url.charset(), "US-ASCII"); +//! assert_eq!(data_url.data(), b"Hello, World!"); +//! assert_eq!(data_url.text(), "Hello, World!"); +//! +//! let mut data_url = DataUrl::new(); +//! data_url.set_media_type(Some("image/png"))?; +//! data_url.set_is_base64_encoded(true); +//! data_url.set_data(&[0x89, 0x50, 0x4E, 0x47]); +//! assert_eq!(data_url.to_string(), "data:image/png;base64,iVBORw=="); +//! # Ok::<(), dataurl::Error>(()) +//! ``` +//! +//! [data: URL processor]: https://fetch.spec.whatwg.org/#data-url-processor + +mod base64; +mod mime; +mod percent; + +use encoding_rs::{Encoding, REPLACEMENT, UTF_16BE, UTF_16LE, WINDOWS_1252}; +use std::borrow::Cow; use std::fmt; -use url::Url; +use std::str::FromStr; -const DEFAULT_MEDIA_TYPE: &'static str = "text/plain"; -const DEFAULT_CHARSET: &'static str = "US-ASCII"; -const TEXTUAL_MEDIA_TYPES: &'static [&str] = &[ - "application/atom+xml", +const DEFAULT_MEDIA_TYPE: &str = "text/plain"; +const DEFAULT_CHARSET: &str = "US-ASCII"; +const TEXTUAL_MEDIA_TYPES: &[&str] = &[ "application/dart", "application/ecmascript", "application/javascript", "application/json", "application/jwt", - "application/rdf+xml", - "application/rss+xml", - "application/soap+xml", - "application/vnd.mozilla.xul+xml", "application/x-javascript", + "application/x-www-form-urlencoded", "application/x-yaml", - "application/xhtml+xml", "application/xml", "application/xml-dtd", - "application/xop+xml", "application/yaml", - "image/svg+xml", - "message/imdn+xml", - "model/x3d+xml", ]; -// TODO: add support for other optional parameters besides charset (filename, etc) -pub struct DataUrl { - media_type: Option, // Media type - charset: Option, // US-ASCII is default, according to the spec - is_base64_encoded: bool, // Indicates if it's a base64-encoded data URL - data: Vec, // Data, bytes, UTF-8 if text - fragment: Option, // #something-at-the-end, None by default -} - -pub enum DataUrlParseError { - UrlParseError, - MalformedDataUrlError, - Base64DecodeError, +/// Errors returned when parsing or modifying a [`DataUrl`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum Error { + /// The input doesn't start with the `data:` scheme. + NotADataUrl, + /// The input starts with `data:`, but isn't a valid URL (e.g. it has a malformed authority). + InvalidUrl, + /// There's no comma separating the metadata from the data. + MissingComma, + /// The data is marked as base64-encoded, but isn't valid base64. + InvalidBase64, + /// The media type isn't of the `type/subtype` form. + InvalidMediaType, + /// The media type parameter has an invalid name or value. + InvalidParameter, + /// The charset is unknown, or can't be used for data URLs. + InvalidCharset, + /// The text contains characters that the data URL's charset can't represent. + UnencodableText, } -impl fmt::Debug for DataUrlParseError { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - fmt.debug_struct("DataUrlParseError").finish() +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match self { + Error::NotADataUrl => "not a data URL", + Error::InvalidUrl => "invalid URL", + Error::MissingComma => "data URL has no comma separating metadata from data", + Error::InvalidBase64 => "data URL contains invalid base64", + Error::InvalidMediaType => "invalid media type", + Error::InvalidParameter => "invalid media type parameter", + Error::InvalidCharset => "invalid or unsupported charset", + Error::UnencodableText => "text can't be represented in the data URL's charset", + }) } } -pub(crate) fn parse_data_url_meta_data( - meta_data_string: String, -) -> (Option, Option, bool) { - let mut media_type: Option = None; - let mut charset: Option = None; - let mut is_base64_encoded: bool = false; - - // Parse meta data - let content_type_items: Vec<&str> = meta_data_string.split(';').collect(); - let mut i: i8 = 0; - for item in &content_type_items { - // Media type has to always come first in data URLs - if i == 0 { - if item.trim().len() > 0 && validate_media_type(item) { - media_type = Some(item.trim().to_lowercase().to_string()); - } - } else { - if !is_base64_encoded && item.trim().to_lowercase().starts_with("charset=") { - // only the first occurence of charset counts - if charset.is_none() { - if let Some(e) = Encoding::for_label_no_replacement((&item[8..]).as_bytes()) { - charset = Some(e.name().to_string()); - } - } - } else if item.trim().eq_ignore_ascii_case("base64") { - is_base64_encoded = true; - } - } +impl std::error::Error for Error {} - i += 1; - } - - (media_type, charset, is_base64_encoded) +/// A parsed (or under construction) data URL. +/// +/// The data is always kept as raw bytes, exactly as it was decoded or set. +/// Serializing (via [`Display`](fmt::Display), so `to_string()` or `format!`) never transcodes it; +/// use [`set_text`](DataUrl::set_text) to store text in the data URL's charset. +#[derive(Clone, PartialEq, Eq, Default)] +pub struct DataUrl { + media_type: Option, // Lowercased type/subtype; None if omitted ("text/plain" implied) + parameters: Vec<(String, String)>, // Media type parameters (charset, etc), names lowercased + is_base64_encoded: bool, + data: Vec, + fragment: Option, } -pub(crate) fn validate_media_type(media_type: &str) -> bool { - // Must contain one slash - media_type.split('/').collect::>().len() == 2 +impl fmt::Debug for DataUrl { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // Data can be megabytes long, so only its length is shown + f.debug_struct("DataUrl") + .field("media_type", &self.media_type) + .field("parameters", &self.parameters) + .field("is_base64_encoded", &self.is_base64_encoded) + .field("data", &format_args!("[{} bytes]", self.data.len())) + .field("fragment", &self.fragment) + .finish() + } } impl DataUrl { + /// Creates an empty data URL (`data:,`). pub fn new() -> DataUrl { - DataUrl { - media_type: None, - charset: None, - is_base64_encoded: false, - data: [].to_vec(), - fragment: None, - } + DataUrl::default() } - // TODO: rename to from_string/from_str/from — look for how it's done for String and similar - pub fn parse(input_str: &str) -> Result { - match Url::parse(input_str) { - Ok(url) => { - let path: String = url.path().to_string(); - if let Some(comma_offset) = path.find(',') { - let fragment: Option<&str> = url.fragment(); - - // Parse meta data - let meta_data_string = String::from(&path[..comma_offset]); - let (media_type, charset, is_base64_encoded) = - parse_data_url_meta_data(meta_data_string); - - // Parse raw data into vector of bytes - let mut d: Vec = percent_decode_str(&path[comma_offset + 1..]).collect(); - if let Some(query) = url.query() { - d.push("?".as_bytes()[0]); - d.append(&mut percent_decode_str(&query).collect()); - } - let mut unable_to_decode_base64: bool = false; - let blob: Vec = if is_base64_encoded { - match URL_SAFE.decode(&d) { - Ok(decoded) => decoded, - Err(_) => { - unable_to_decode_base64 = true; - [].to_vec() - } - } - } else { - d - }; - - if unable_to_decode_base64 { - return Err(DataUrlParseError::Base64DecodeError); - } - - Ok(DataUrl { - media_type: media_type, - charset: charset, - is_base64_encoded: is_base64_encoded, - data: blob, - fragment: if let Some(f) = fragment { - Some(f.to_string()) - } else { - None - }, - }) - } else { - Err(DataUrlParseError::MalformedDataUrlError) - } - } - Err(_) => Err(DataUrlParseError::UrlParseError), - } - } + /// Parses a data URL the way browsers do. + /// + /// Like URL parsers, this strips leading and trailing spaces and control characters, + /// and ignores tabs and newlines anywhere in the input. + pub fn parse(input: &str) -> Result { + let input = input.trim_matches(|c: char| c <= ' '); + let input: Cow = if input.contains(['\t', '\n', '\r']) { + Cow::Owned(input.replace(['\t', '\n', '\r'], "")) + } else { + Cow::Borrowed(input) + }; - pub fn is_binary(&self) -> bool { - if self.media_type.is_none() { - return false; + match input.get(..5) { + Some(scheme) if scheme.eq_ignore_ascii_case("data:") => {} + _ => return Err(Error::NotADataUrl), } - let current_media_type: &str = &self.media_type.as_ref().unwrap(); - let is_textual: bool = if current_media_type.split('/').collect::>()[0] - .eq_ignore_ascii_case("text") - { - true + let (rest, fragment) = match input[5..].split_once('#') { + Some((rest, fragment)) => (rest, Some(fragment.to_string())), + None => (&input[5..], None), + }; + + // Hierarchical forms like "data://host/path" are rare, but they're valid URLs with their own rules + let rest: Cow = if rest.starts_with('/') { + Cow::Owned(normalize_hierarchical_part(rest)?) + } else { + Cow::Borrowed(rest) + }; + + let (meta_data, encoded_data) = rest.split_once(',').ok_or(Error::MissingComma)?; + + // The metadata part isn't percent-decoded, but URL parsers do percent-encode some of it + let mut mime_type = String::with_capacity(meta_data.len()); + percent::encode_into( + &mut mime_type, + meta_data.as_bytes(), + percent::in_c0_control_set, + ); + let mut mime_type: &str = mime_type.trim_matches(|c: char| c.is_ascii_whitespace()); + + let mut data: Vec = percent::decode(encoded_data.as_bytes()); + + let is_base64_encoded = match strip_base64_marker(mime_type) { + Some(stripped) => { + data = base64::decode_forgiving(&data).ok_or(Error::InvalidBase64)?; + mime_type = stripped; + true + } + None => false, + }; + + let (media_type, parameters) = if mime_type.starts_with(';') { + // The media type was omitted, but parameters may be present + match mime::parse(&format!("{DEFAULT_MEDIA_TYPE}{mime_type}")) { + // Keep it explicit if there are no parameters, otherwise it'd revert to the default charset + Some(m) if m.parameters.is_empty() => (Some(m.essence), m.parameters), + Some(m) => (None, m.parameters), + None => (None, Vec::new()), + } } else { - TEXTUAL_MEDIA_TYPES - .iter() - .find(|mt| current_media_type.eq_ignore_ascii_case(mt)) - .is_some() + match mime::parse(mime_type) { + Some(m) => (Some(m.essence), m.parameters), + None => (None, Vec::new()), + } }; + Ok(DataUrl { + media_type, + parameters, + is_base64_encoded, + data, + fragment, + }) + } + + /// Returns true if the media type isn't a textual one. + pub fn is_binary(&self) -> bool { + let media_type: &str = self.media_type(); + let is_textual = media_type.starts_with("text/") + || media_type.ends_with("+xml") + || media_type.ends_with("+json") + || TEXTUAL_MEDIA_TYPES.contains(&media_type); + !is_textual } + /// The media type's essence (`type/subtype`, lowercase), `"text/plain"` if omitted. pub fn media_type(&self) -> &str { - if let Some(mt) = &self.media_type { - mt - } else { - DEFAULT_MEDIA_TYPE + self.media_type.as_deref().unwrap_or(DEFAULT_MEDIA_TYPE) + } + + /// The media type's essence, `None` if omitted. + pub fn media_type_no_default(&self) -> Option<&str> { + self.media_type.as_deref() + } + + /// The full MIME type including parameters, the way browsers report it, + /// e.g. `"text/plain;charset=US-ASCII"` for `data:,`. + pub fn mime_type(&self) -> String { + if self.media_type.is_none() && self.parameters.is_empty() { + return format!("{DEFAULT_MEDIA_TYPE};charset={DEFAULT_CHARSET}"); } + + let mut mime_type = self.media_type().to_string(); + mime::serialize_parameters(&mut mime_type, &self.parameters); + mime_type } - pub fn media_type_no_default(&self) -> Option { - if let Some(mt) = &self.media_type { - Some(mt.to_string()) - } else { - None + /// Sets the media type essence (e.g. `"image/png"`), or removes it with `None`. + /// Parameters such as charset are kept, and can't be part of the given value. + pub fn set_media_type(&mut self, media_type: Option<&str>) -> Result<(), Error> { + match media_type { + Some(media_type) => { + let (top_level_type, subtype) = media_type + .trim_matches(mime::is_http_whitespace) + .split_once('/') + .ok_or(Error::InvalidMediaType)?; + if !mime::is_valid_essence(top_level_type) || !mime::is_valid_essence(subtype) { + return Err(Error::InvalidMediaType); + } + self.media_type = Some(format!("{top_level_type}/{subtype}").to_ascii_lowercase()); + } + None => self.media_type = None, } + + Ok(()) } - pub fn set_media_type(&mut self, new_media_type: Option) -> bool { - if let Some(mt) = new_media_type { - if mt.trim().len() > 0 && validate_media_type(&mt) { - self.media_type = Some(mt.to_string()); - true - } else { - // Empty media type makes it fall back to default (text/plain) - self.media_type = None; - false + /// Iterates over media type parameters as `(name, value)` pairs, in order. + pub fn parameters(&self) -> impl Iterator { + self.parameters + .iter() + .map(|(n, v)| (n.as_str(), v.as_str())) + } + + /// Returns the value of a media type parameter (names are case-insensitive). + pub fn parameter(&self, name: &str) -> Option<&str> { + self.parameters + .iter() + .find(|(n, _)| n.eq_ignore_ascii_case(name)) + .map(|(_, v)| v.as_str()) + } + + /// Sets or replaces a media type parameter, or removes it with `None`. + /// + /// Values may contain any printable ASCII character except `,` and `#`, + /// which can't be represented in the metadata part of a data URL. + /// For charset, prefer [`set_charset`](DataUrl::set_charset), which validates the value. + pub fn set_parameter(&mut self, name: &str, value: Option<&str>) -> Result<(), Error> { + if !mime::is_valid_essence(name) { + return Err(Error::InvalidParameter); + } + let name = name.to_ascii_lowercase(); + + match value { + Some(value) => { + if !value + .bytes() + .all(|b| matches!(b, b' '..=b'~') && b != b',' && b != b'#') + { + return Err(Error::InvalidParameter); + } + match self.parameters.iter_mut().find(|(n, _)| *n == name) { + Some((_, v)) => *v = value.to_string(), + None => self.parameters.push((name, value.to_string())), + } } - } else { - self.media_type = None; - true + None => self.parameters.retain(|(n, _)| *n != name), } + + Ok(()) } + /// The charset parameter, `"US-ASCII"` if not present. pub fn charset(&self) -> &str { - if let Some(c) = &self.charset { - c - } else { - DEFAULT_CHARSET - } + self.charset_no_default().unwrap_or(DEFAULT_CHARSET) } - pub fn charset_no_default(&self) -> Option { - if let Some(c) = &self.charset { - Some(c.to_string()) - } else { - None - } + /// The charset parameter, `None` if not present. + pub fn charset_no_default(&self) -> Option<&str> { + self.parameter("charset") } - pub fn set_charset(&mut self, new_charset: Option) -> bool { - if let Some(nc) = new_charset { - // Validate the input - if let Some(e) = Encoding::for_label_no_replacement(nc.as_bytes()) { - self.charset = Some(e.name().to_string()); - true - } else { - // Since browsers fall back to US-ASCII, so does this - self.charset = None; - false + /// Sets the charset parameter, or removes it with `None`. + /// + /// Any label from the WHATWG Encoding Standard is accepted, and gets stored as the + /// encoding's canonical name (e.g. `"utf8"` becomes `"UTF-8"`). + /// This only changes the label; use [`set_text`](DataUrl::set_text) to encode text in it. + pub fn set_charset(&mut self, charset: Option<&str>) -> Result<(), Error> { + match charset { + Some(label) => { + let encoding = Encoding::for_label_no_replacement(label.trim().as_bytes()) + .ok_or(Error::InvalidCharset)?; + self.set_parameter("charset", Some(encoding.name())) } - } else { - // Unset - self.charset = None; - true + None => self.set_parameter("charset", None), } } - // TODO: ditch get/set_is_base64_encode and implement two separate functions, to_precent_encoded_string, and to_base64_encoded_string? - // TODO: ^ if taken that path, should was_input_base64_encoded() added, None by default, Option after parse() is used, added? + /// The encoding used for decoding and encoding text, based on the charset parameter. + /// Missing or unknown charsets result in windows-1252, which is what `US-ASCII` maps to. + pub fn encoding(&self) -> &'static Encoding { + self.charset_no_default() + .and_then(|label| Encoding::for_label(label.as_bytes())) + .unwrap_or(WINDOWS_1252) + } + /// Whether the data gets serialized as base64 (and whether it was, when parsed). pub fn is_base64_encoded(&self) -> bool { self.is_base64_encoded } - pub fn set_is_base64_encoded(&mut self, new_is_base64_encoded: bool) { - self.is_base64_encoded = new_is_base64_encoded; + /// Sets whether the data gets serialized as base64 or percent-encoded. + pub fn set_is_base64_encoded(&mut self, is_base64_encoded: bool) { + self.is_base64_encoded = is_base64_encoded; } + /// The raw data. pub fn data(&self) -> &[u8] { &self.data } - pub fn text(&self) -> String { - // This can never really fail - if let Some(encoding) = Encoding::for_label_no_replacement( - self.charset - .as_ref() - .unwrap_or(&DEFAULT_CHARSET.to_string()) - .as_bytes(), - ) { - let (decoded, _, _) = encoding.decode(&self.data); - decoded.to_string() - } else { - "".to_string() - } + /// Consumes the data URL and returns its raw data. + pub fn into_data(self) -> Vec { + self.data } - /* - // TODO: add new_text_charset argument? - pub fn set_text(&mut self, new_text: &str) { - if self.charset == Some("UTF-8".to_string()) { - self.data = new_text.as_bytes().to_vec(); - } else { - if let Some(encoding) = Encoding::for_label_no_replacement( - self.charset - .as_ref() - .unwrap_or(&DEFAULT_CHARSET.to_string()) - .as_bytes(), - ) { - let (decoded, _, _) = encoding.decode(&new_text.as_bytes()); - self.data = decoded.as_bytes().to_vec(); - } - } - } - */ + /// Sets the raw data, which will be serialized byte-for-byte. + pub fn set_data(&mut self, data: &[u8]) { + self.data = data.to_vec(); + } - pub fn set_data(&mut self, new_data: &[u8]) { - self.data = new_data.to_vec(); + /// The data decoded as text using [`encoding`](DataUrl::encoding). + /// A byte order mark, if present, takes precedence over the charset. + pub fn text(&self) -> Cow<'_, str> { + self.encoding().decode(&self.data).0 } - pub fn fragment(&self) -> Option { - if let Some(f) = &self.fragment { - Some(f.to_string()) + /// Sets the data to the given text, encoded using [`encoding`](DataUrl::encoding). + /// + /// Fails if the text contains characters that can't be represented in that encoding + /// (e.g. emoji in windows-1252, the default); set the charset to UTF-8 to store anything. + pub fn set_text(&mut self, text: &str) -> Result<(), Error> { + let encoding = self.encoding(); + + self.data = if encoding == UTF_16LE { + text.encode_utf16().flat_map(u16::to_le_bytes).collect() + } else if encoding == UTF_16BE { + text.encode_utf16().flat_map(u16::to_be_bytes).collect() + } else if encoding == REPLACEMENT { + return Err(Error::UnencodableText); } else { - None - } + let (encoded, _, had_unmappable_characters) = encoding.encode(text); + if had_unmappable_characters { + return Err(Error::UnencodableText); + } + encoded.into_owned() + }; + + Ok(()) + } + + /// The fragment (the part after `#`), `None` if absent. + pub fn fragment(&self) -> Option<&str> { + self.fragment.as_deref() } - pub fn set_fragment(&mut self, new_fragment: Option) { - self.fragment = new_fragment; + /// Sets the fragment, or removes it with `None`. + pub fn set_fragment(&mut self, fragment: Option<&str>) { + self.fragment = fragment.map(str::to_string); } +} - // TODO: rename it to as_str/to_str, make it return a &str instead of String - // TODO: make it an Option(Result?), throw error in case is_base64_encoded=false, and charset!=default|utf8 - pub fn to_string(&self) -> String { - let mut result: String = String::from("data:"); +impl fmt::Display for DataUrl { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut output = String::from("data:"); - if let Some(mt) = &self.media_type { - result += &mt; + if let Some(media_type) = &self.media_type { + output.push_str(media_type); } + mime::serialize_parameters(&mut output, &self.parameters); - if let Some(c) = &self.charset { - // windows-1252 is another name for US-ASCII, the default charset for data URLs - if c != "windows-1252" { - result += ";charset="; - result += &c; - } + if self.is_base64_encoded { + output.push_str(";base64,"); + base64::encode_into(&mut output, &self.data); + } else { + output.push(','); + percent::encode_into(&mut output, &self.data, percent::in_data_set); } - { - if self.is_base64_encoded { - result += ";base64"; + if let Some(fragment) = &self.fragment { + output.push('#'); + percent::encode_into(&mut output, fragment.as_bytes(), percent::in_fragment_set); + } + + f.write_str(&output) + } +} + +impl FromStr for DataUrl { + type Err = Error; + + fn from_str(input: &str) -> Result { + DataUrl::parse(input) + } +} + +impl TryFrom<&str> for DataUrl { + type Error = Error; + + fn try_from(input: &str) -> Result { + DataUrl::parse(input) + } +} + +/// If the metadata ends with ";base64" (optionally with spaces after the semicolon), +/// returns the metadata without it. +fn strip_base64_marker(mime_type: &str) -> Option<&str> { + let split_at = mime_type.len().checked_sub(6)?; + if !mime_type.is_char_boundary(split_at) { + return None; + } + let (head, tail) = mime_type.split_at(split_at); + + if !tail.eq_ignore_ascii_case("base64") { + return None; + } + + head.trim_end_matches(' ').strip_suffix(';') +} + +/// Normalizes the part of a data URL that comes after "data:" when it starts with "/", +/// which makes it a hierarchical URL (with an optional authority) instead of an opaque one. +/// Returns what URL serializers would produce for it, minus the scheme and fragment. +fn normalize_hierarchical_part(input: &str) -> Result { + let mut output = String::with_capacity(input.len()); + let mut rest = input; + + if let Some(after_slashes) = input.strip_prefix("//") { + let authority_end = after_slashes + .find(['/', '?']) + .unwrap_or(after_slashes.len()); + let authority = &after_slashes[..authority_end]; + rest = &after_slashes[authority_end..]; + + let (userinfo, host_and_port) = match authority.rsplit_once('@') { + Some((userinfo, host_and_port)) => (Some(userinfo), host_and_port), + None => (None, authority), + }; + + let (host, port) = match host_and_port.strip_prefix('[') { + // IPv6 address + Some(after_bracket) => { + let end = after_bracket.find(']').ok_or(Error::InvalidUrl)?; + let address = &after_bracket[..end]; + if address.is_empty() + || !address + .chars() + .all(|c| c.is_ascii_hexdigit() || c == ':' || c == '.') + { + return Err(Error::InvalidUrl); + } + let after = &after_bracket[end + 1..]; + let port = match after.strip_prefix(':') { + Some(port) => port, + None if after.is_empty() => "", + None => return Err(Error::InvalidUrl), + }; + (&host_and_port[..end + 2], port) } - result += ","; - - if self.data.len() > 0 { - if self.is_binary() { - // Just encode as base64 or URI if data is binary - if self.is_base64_encoded { - result += &URL_SAFE.encode(&self.data); - } else { - result += &percent_encode(&self.data, NON_ALPHANUMERIC).to_string(); - } - } else { - // Charset only matters for textual data - let data_as_utf8_string: String = - String::from_utf8_lossy(&self.data).to_string(); - let fallback_charset: String = if data_as_utf8_string.is_ascii() { - DEFAULT_CHARSET.to_string() - } else { - "UTF-8".to_string() - }; - - if let Some(encoding) = Encoding::for_label_no_replacement( - self.charset - .as_ref() - .unwrap_or(&fallback_charset) - .as_bytes(), - ) { - let (encoded, _, _) = encoding.encode(&data_as_utf8_string); - - if self.is_base64_encoded { - result += &URL_SAFE.encode(&encoded.to_vec()); - } else { - result += - &percent_encode(&encoded.to_vec(), NON_ALPHANUMERIC).to_string(); - } - } + None => { + let (host, port) = host_and_port.split_once(':').unwrap_or((host_and_port, "")); + if host.contains(['\0', ' ', '<', '>', '@', '[', '\\', ']', '^', '|']) { + return Err(Error::InvalidUrl); } + (host, port) } + }; + + let port_is_valid = port.is_empty() + || (port.bytes().all(|b| b.is_ascii_digit()) && port.parse::().is_ok()); + if !port_is_valid || (host.is_empty() && (userinfo.is_some() || !port.is_empty())) { + return Err(Error::InvalidUrl); + } + + output.push_str("//"); + if let Some(userinfo) = userinfo.filter(|u| !u.is_empty()) { + output.push_str(userinfo); + output.push('@'); } + output.push_str(host); + if !port.is_empty() { + output.push(':'); + output.push_str(port); + } + } + + // Resolve "." and ".." path segments + let (path, query) = rest.split_at(rest.find('?').unwrap_or(rest.len())); + if let Some(path) = path.strip_prefix('/') { + let is_single_dot = |s: &str| s == "." || s.eq_ignore_ascii_case("%2e"); + let is_double_dot = |s: &str| { + matches!( + s.to_ascii_lowercase().as_str(), + ".." | ".%2e" | "%2e." | "%2e%2e" + ) + }; - if let Some(f) = &self.fragment { - result += "#"; - // TODO: need to deal with encoding here as well - result += &utf8_percent_encode(f, NON_ALPHANUMERIC).to_string(); + let segments: Vec<&str> = path.split('/').collect(); + let mut resolved: Vec<&str> = Vec::with_capacity(segments.len()); + for (i, segment) in segments.iter().enumerate() { + let is_last = i == segments.len() - 1; + if is_double_dot(segment) { + resolved.pop(); + if is_last { + resolved.push(""); + } + } else if is_single_dot(segment) { + if is_last { + resolved.push(""); + } + } else { + resolved.push(segment); + } } - result + for segment in resolved { + output.push('/'); + output.push_str(segment); + } } + output.push_str(query); + + Ok(output) } diff --git a/src/main.rs b/src/main.rs index 83e8e22..ce8a0a2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,212 +1,244 @@ -extern crate clap; - -use atty::Stream; -use clap::{crate_authors, crate_description, crate_version, App, Arg}; use dataurl::DataUrl; -use std::env; +use std::ffi::OsString; use std::fs; -use std::io::{self, prelude::*, Write}; +use std::io::{self, IsTerminal, Read, Write}; +use std::process::exit; -pub fn read_stdin() -> Vec { - let mut buffer: Vec = vec![]; +const NAME: &str = env!("CARGO_PKG_NAME"); +const VERSION: &str = env!("CARGO_PKG_VERSION"); - match io::stdin().lock().read_to_end(&mut buffer) { - Ok(_) => buffer, - Err(_) => buffer, - } +fn usage() -> String { + format!("Usage: {NAME} [OPTIONS] [INPUT]") } -fn main() { - let app = App::new(env!("CARGO_PKG_NAME")) - .version(crate_version!()) - .author(format!("\n{}", crate_authors!("\n")).as_str()) - .about(crate_description!()) - .arg( - Arg::with_name("base64") - .short("b") - .long("base64") - .multiple(false) - .help("Enforces base64 encoding"), - ) - .arg( - Arg::with_name("ENCODING") - .short("c") - .long("charset") - .multiple(false) - .takes_value(true) - .help("Sets custom encoding parameter"), - ) - .arg( - Arg::with_name("decode") - .short("d") - .long("decode") - .multiple(false) - .help("Toggles decode mode on"), - ) - .arg( - Arg::with_name("FRAGMENT") - .short("f") - .long("fragment") - .multiple(false) - .takes_value(true) - .help("Appends URL fragment"), - ) - .arg( - Arg::with_name("INPUT FILE") - .short("i") - .long("input-file") - .multiple(false) - .takes_value(true) - .help("Provides input file"), - ) - .arg( - Arg::with_name("OUTPUT FILE") - .short("o") - .long("output-file") - .multiple(false) - .takes_value(true) - .help("Specifies output file"), - ) - .arg( - Arg::with_name("MEDIA TYPE") - .short("t") - .long("media-type") - .multiple(false) - .takes_value(true) - .help("Sets custom media type"), - ) - .arg(Arg::with_name("INPUT").help("Input string").required(false)) - .get_matches(); - - ////////////////////////////////////////////////////////////////////////// - - let decode_mode_enabled: bool = app.is_present("decode"); - let string_input_set: bool = app.is_present("INPUT"); - // let stdin_is_a_tty: bool = !io::stdio::stdin_raw().isatty(); - let stdout_is_a_tty: bool = atty::is(Stream::Stdout); - let mut file_input_set: bool = app.is_present("INPUT FILE"); - let mut file_output_set: bool = app.is_present("OUTPUT FILE"); - let input_file_path: &str = if file_input_set { - app.value_of("INPUT FILE").unwrap() - } else { - "-" - }; - let output_file_path: &str = if file_output_set { - app.value_of("OUTPUT FILE").unwrap() - } else { - "-" - }; - if file_input_set && input_file_path == "-" { - file_input_set = false; - } - if file_output_set && output_file_path == "-" { - file_output_set = false; - } - let file_input_set = file_input_set; - let file_output_set = file_output_set; +fn help() -> String { + format!( + "{NAME} {VERSION} +{description} + +{usage} + +Arguments: + [INPUT] Text to encode, or data URL to decode (read from STDIN if omitted) + +Options: + -b, --base64 Encode data using base64 + -c, --charset Set charset (INPUT text gets converted to it) + -d, --decode Decode a data URL and output its data + -f, --fragment Append URL fragment + -i, --input-file Read input from FILE (\"-\" for STDIN) + -o, --output-file Write output to FILE (\"-\" for STDOUT) + -t, --media-type Set media type + -h, --help Print help + -V, --version Print version +", + description = env!("CARGO_PKG_DESCRIPTION"), + usage = usage(), + ) +} - ////////////////////////////////////////////////////////////////////////// +fn fail(message: &str) -> ! { + eprintln!("error: {message}"); + exit(1); +} - if string_input_set && file_input_set { - eprintln!("error: Both file and argument inputs provided"); - std::process::exit(1); - } +fn fail_with_usage(message: &str) -> ! { + eprintln!( + "error: {message}\n\n{}\n\nFor more information, try '--help'.", + usage() + ); + exit(2); +} + +#[derive(Default)] +struct Options { + base64: bool, + charset: Option, + decode: bool, + fragment: Option, + input_file: Option, + output_file: Option, + media_type: Option, + input: Option, +} + +fn parse_args(args: impl Iterator) -> Options { + let mut options = Options::default(); + let mut args = args.peekable(); + let mut only_positional = false; - if !stdout_is_a_tty && file_output_set { - eprintln!("error: Both stdout and argument output provided"); - std::process::exit(1); + fn into_string(value: OsString) -> String { + value + .into_string() + .unwrap_or_else(|_| fail("Argument is not valid UTF-8")) } - ////////////////////////////////////////////////////////////////////////// + while let Some(arg) = args.next() { + let arg_str = arg.to_string_lossy().to_string(); - let input: Vec = if string_input_set { - app.value_of("INPUT").unwrap().as_bytes().to_vec() - } else if file_input_set { - match fs::read(input_file_path) { - Ok(input_file_data) => input_file_data, - Err(_) => { - eprintln!("error: Unable to read input file '{}'", input_file_path); - std::process::exit(1); + if only_positional || arg_str == "-" || !arg_str.starts_with('-') { + if options.input.is_some() { + fail_with_usage(&format!("unexpected argument '{arg_str}'")); } + options.input = Some(into_string(arg)); + continue; } - } else { - // TODO: make it hang here, waiting on input from STDIN the way GNU's `base64` or `cat` do - read_stdin() - }; - ////////////////////////////////////////////////////////////////////////// - - if decode_mode_enabled { - // TODO: ideally the program needs to check the current terminal locale (encoding), and not just assume it's UTF-8 - let input_as_string: String = String::from_utf8_lossy(&input).to_string(); - - std::process::exit(match DataUrl::parse(&input_as_string) { - Ok(data_url) => { - if !stdout_is_a_tty || file_output_set || data_url.is_binary() { - // Write raw bytes if the output is a file, or if the contents of this data URL has binary format - if file_output_set { - let mut handle = fs::File::create(output_file_path).unwrap(); - handle.write_all(data_url.data()).unwrap(); - } else { - let stdout = io::stdout(); - let mut handle = stdout.lock(); - handle.write_all(data_url.data()).unwrap(); - } - } else { - // When printing the result directly into the terminal, we have to convert data into UTF-8 (must account for non-US-ASCII/UTF-8 charsets) - print!("{}", data_url.text()); - } - 0 + if arg_str == "--" { + only_positional = true; + continue; + } + + // Support both "--option value" and "--option=value", as well as "-o value" and "-ovalue" + let (flag, inline_value): (String, Option) = if arg_str.starts_with("--") { + match arg_str.split_once('=') { + Some((flag, value)) => (flag.to_string(), Some(value.to_string())), + None => (arg_str.clone(), None), } - Err(err) => { - eprintln!("error: {:?}", err); - 1 + } else if arg_str.chars().count() > 2 { + let mut chars = arg_str.chars(); + let flag: String = chars.by_ref().take(2).collect(); + (flag, Some(chars.collect())) + } else { + (arg_str.clone(), None) + }; + + let mut value = |name: &str| -> OsString { + match &inline_value { + Some(value) => OsString::from(value), + None => args.next().unwrap_or_else(|| { + fail_with_usage(&format!("a value is required for '{name}'")) + }), } - }); - } else { - let mut data_url = DataUrl::new(); - - data_url.set_data(&input); + }; - if app.is_present("base64") { - data_url.set_is_base64_encoded(true); + match flag.as_str() { + "-b" | "--base64" | "-d" | "--decode" | "-h" | "--help" | "-V" | "--version" + if inline_value.is_some() => + { + fail_with_usage(&format!("unexpected value for '{flag}'")) + } + "-b" | "--base64" => options.base64 = true, + "-d" | "--decode" => options.decode = true, + "-h" | "--help" => { + print!("{}", help()); + exit(0); + } + "-V" | "--version" => { + println!("{NAME} {VERSION}"); + exit(0); + } + "-c" | "--charset" => options.charset = Some(into_string(value("--charset"))), + "-f" | "--fragment" => options.fragment = Some(into_string(value("--fragment"))), + "-i" | "--input-file" => options.input_file = Some(value("--input-file")), + "-o" | "--output-file" => options.output_file = Some(value("--output-file")), + "-t" | "--media-type" => options.media_type = Some(into_string(value("--media-type"))), + _ => fail_with_usage(&format!("unexpected argument '{arg_str}'")), } + } - if app.is_present("ENCODING") { - let charset: &str = app.value_of("ENCODING").unwrap(); - let success: bool = data_url.set_charset(Some(charset.to_string())); + options +} - if !success { - eprintln!("error: Invalid encoding '{}'", charset); - std::process::exit(1); +fn write_output(output_file: &Option, data: &[u8]) { + let result = match output_file { + Some(path) => fs::write(path, data).map_err(|e| (e, path.to_string_lossy())), + None => { + let mut stdout = io::stdout().lock(); + match stdout.write_all(data).and_then(|_| stdout.flush()) { + // Being piped into something like `head` is not an error + Err(e) if e.kind() == io::ErrorKind::BrokenPipe => Ok(()), + result => result.map_err(|e| (e, "STDOUT".into())), } + } + }; - // TODO: encode data into provided charset, if different - } else { - // TODO: ideally the program needs to check the current terminal locale (encoding), and not just assume it's UTF-8 + if let Err((e, destination)) = result { + fail(&format!("Unable to write to '{destination}': {e}")); + } +} - // Automatically enforce ;charset=UTF-8 for non-ascii argument inputs - if string_input_set && !String::from_utf8_lossy(&input).to_string().is_ascii() { - data_url.set_charset(Some("UTF-8".to_string())); - } +fn main() { + let mut options = parse_args(std::env::args_os().skip(1)); + + // "-" means STDIN/STDOUT, same as not specifying the option at all + options.input_file = options.input_file.filter(|path| path != "-"); + options.output_file = options.output_file.filter(|path| path != "-"); + + if options.input.is_some() && options.input_file.is_some() { + fail("Both file and argument inputs provided"); + } + + let input: Vec = if let Some(input) = &options.input { + input.as_bytes().to_vec() + } else if let Some(path) = &options.input_file { + fs::read(path).unwrap_or_else(|e| { + fail(&format!( + "Unable to read input file '{}': {e}", + path.to_string_lossy() + )) + }) + } else { + let mut buffer: Vec = Vec::new(); + if let Err(e) = io::stdin().lock().read_to_end(&mut buffer) { + fail(&format!("Unable to read STDIN: {e}")); } + buffer + }; - if app.is_present("MEDIA TYPE") { - let media_type: &str = app.value_of("MEDIA TYPE").unwrap(); - let success: bool = data_url.set_media_type(Some(media_type.to_string())); + if options.decode { + let data_url = DataUrl::parse(&String::from_utf8_lossy(&input)) + .unwrap_or_else(|e| fail(&e.to_string())); - if !success { - eprintln!("error: Invalid media type '{}'", media_type); - std::process::exit(1); - } + if options.output_file.is_none() && io::stdout().is_terminal() && !data_url.is_binary() { + // Text printed into a terminal gets converted from the data URL's charset + // TODO: check the terminal's encoding instead of assuming it's UTF-8 + write_output(&None, data_url.text().as_bytes()); + } else { + write_output(&options.output_file, data_url.data()); } + } else { + let mut data_url = DataUrl::new(); + + data_url.set_is_base64_encoded(options.base64); - if app.is_present("FRAGMENT") { - data_url.set_fragment(Some(app.value_of("FRAGMENT").unwrap().to_string())); + if let Some(media_type) = &options.media_type { + data_url + .set_media_type(Some(media_type)) + .unwrap_or_else(|_| fail(&format!("Invalid media type '{media_type}'"))); } - println!("{}", data_url.to_string()); + match (&options.charset, &options.input) { + (Some(charset), text) => { + data_url + .set_charset(Some(charset)) + .unwrap_or_else(|_| fail(&format!("Invalid encoding '{charset}'"))); + + match text { + // Text given as an argument is UTF-8, so it gets converted to the charset + Some(text) => data_url.set_text(text).unwrap_or_else(|_| { + fail(&format!( + "Input text can't be represented in {}", + data_url.charset() + )) + }), + // Files and STDIN are taken as-is + None => data_url.set_data(&input), + } + } + (None, Some(text)) => { + data_url.set_data(text.as_bytes()); + if !text.is_ascii() { + data_url.set_charset(Some("UTF-8")).unwrap(); + } + } + (None, None) => data_url.set_data(&input), + } + + if let Some(fragment) = &options.fragment { + data_url.set_fragment(Some(fragment)); + } - std::process::exit(0); + write_output(&options.output_file, format!("{data_url}\n").as_bytes()); } } diff --git a/src/mime.rs b/src/mime.rs new file mode 100644 index 0000000..0e68249 --- /dev/null +++ b/src/mime.rs @@ -0,0 +1,225 @@ +//! MIME type parsing and serialization, as defined by the WHATWG MIME Sniffing +//! Standard (). + +pub(crate) type Parameters = Vec<(String, String)>; + +pub(crate) struct MimeType { + pub essence: String, // Lowercased "type/subtype" + pub parameters: Parameters, +} + +pub(crate) fn is_http_whitespace(c: char) -> bool { + matches!(c, '\t' | '\n' | '\r' | ' ') +} + +pub(crate) fn is_token_char(c: char) -> bool { + c.is_ascii_alphanumeric() || "!#$%&'*+-.^_`|~".contains(c) +} + +fn is_quoted_string_token_char(c: char) -> bool { + matches!(c, '\t' | ' '..='~' | '\u{80}'..='\u{ff}') +} + +pub(crate) fn is_valid_essence(type_or_subtype: &str) -> bool { + !type_or_subtype.is_empty() && type_or_subtype.chars().all(is_token_char) +} + +/// Returns None if the input is not a valid MIME type. +pub(crate) fn parse(input: &str) -> Option { + let s = input.trim_matches(is_http_whitespace); + let b = s.as_bytes(); + + let (top_level_type, rest) = s.split_once('/')?; + if !is_valid_essence(top_level_type) { + return None; + } + + let subtype_end = rest.find(';').unwrap_or(rest.len()); + let subtype = rest[..subtype_end].trim_end_matches(is_http_whitespace); + if !is_valid_essence(subtype) { + return None; + } + + let mut mime_type = MimeType { + essence: format!( + "{}/{}", + top_level_type.to_ascii_lowercase(), + subtype.to_ascii_lowercase() + ), + parameters: Vec::new(), + }; + + // All delimiters are ASCII, so byte offsets always land on char boundaries + let mut pos = top_level_type.len() + 1 + subtype_end; + while pos < b.len() { + // Skip past the semicolon and any whitespace after it + pos += 1; + while pos < b.len() && is_http_whitespace(b[pos] as char) { + pos += 1; + } + + let name_start = pos; + while pos < b.len() && b[pos] != b';' && b[pos] != b'=' { + pos += 1; + } + let name = s[name_start..pos].to_ascii_lowercase(); + + if pos < b.len() { + if b[pos] == b';' { + continue; + } + pos += 1; // Skip past "=" + } + if pos >= b.len() { + break; + } + + let value: String = if b[pos] == b'"' { + let (value, new_pos) = collect_http_quoted_string(s, pos); + pos = new_pos; + while pos < b.len() && b[pos] != b';' { + pos += 1; + } + value + } else { + let value_start = pos; + while pos < b.len() && b[pos] != b';' { + pos += 1; + } + let value = s[value_start..pos].trim_end_matches(is_http_whitespace); + if value.is_empty() { + continue; + } + value.to_string() + }; + + if !name.is_empty() + && name.chars().all(is_token_char) + && value.chars().all(is_quoted_string_token_char) + && !mime_type.parameters.iter().any(|(n, _)| *n == name) + { + mime_type.parameters.push((name, value)); + } + } + + Some(mime_type) +} + +// https://fetch.spec.whatwg.org/#collect-an-http-quoted-string (with extract-value set to true) +fn collect_http_quoted_string(s: &str, mut pos: usize) -> (String, usize) { + let b = s.as_bytes(); + let mut value = String::new(); + + pos += 1; // Skip past the opening quote + loop { + let start = pos; + while pos < b.len() && b[pos] != b'"' && b[pos] != b'\\' { + pos += 1; + } + value.push_str(&s[start..pos]); + + if pos >= b.len() { + break; + } + + let quote_or_backslash = b[pos]; + pos += 1; + if quote_or_backslash == b'\\' { + match s[pos..].chars().next() { + Some(c) => { + value.push(c); + pos += c.len_utf8(); + } + None => { + value.push('\\'); + break; + } + } + } else { + break; + } + } + + (value, pos) +} + +pub(crate) fn serialize_parameters(output: &mut String, parameters: &[(String, String)]) { + for (name, value) in parameters { + output.push(';'); + output.push_str(name); + output.push('='); + if !value.is_empty() && value.chars().all(is_token_char) { + output.push_str(value); + } else { + output.push('"'); + for c in value.chars() { + if c == '"' || c == '\\' { + output.push('\\'); + } + output.push(c); + } + output.push('"'); + } + } +} + +#[cfg(test)] +#[path = "../tests/common/json.rs"] +mod json; + +#[cfg(test)] +mod tests { + use super::json::{self, Json}; + use super::*; + use encoding_rs::Encoding; + + fn run_wpt_mime_type_tests(fixture: &str) { + let mut count = 0; + + for test in json::parse(fixture).as_array() { + // Strings in between test objects are section titles + if let Json::Object(_) = test { + let input = test.get("input").as_str(); + let expected = test.get("output"); + + match parse(input) { + Some(mime_type) => { + let mut serialized = mime_type.essence.clone(); + serialize_parameters(&mut serialized, &mime_type.parameters); + assert_eq!( + Some(serialized.as_str()), + expected.as_opt_str(), + "{input:?}" + ); + + // The "encoding" fields aren't in every fixture + if let Some(expected_encoding) = test.get_opt("encoding") { + let encoding = mime_type + .parameters + .iter() + .find(|(name, _)| name == "charset") + .and_then(|(_, value)| Encoding::for_label(value.as_bytes())) + .map(|e| e.name()); + assert_eq!(encoding, expected_encoding.as_opt_str(), "{input:?}"); + } + } + None => assert!(expected.is_null(), "{input:?} must not be parsed"), + } + + count += 1; + } + } + + assert!(count > 0); + } + + #[test] + fn wpt_mime_types() { + run_wpt_mime_type_tests(include_str!("../tests/wpt/mime-types.json")); + } + + #[test] + fn wpt_generated_mime_types() { + run_wpt_mime_type_tests(include_str!("../tests/wpt/generated-mime-types.json")); + } +} diff --git a/src/percent.rs b/src/percent.rs new file mode 100644 index 0000000..7a2f8cb --- /dev/null +++ b/src/percent.rs @@ -0,0 +1,83 @@ +//! Percent-encoding (). + +const HEX: &[u8; 16] = b"0123456789ABCDEF"; + +/// Bytes that get percent-encoded in the data portion of a data URL. +/// Besides what URLs require (controls, space, non-ASCII, "%", "#"), this also covers +/// characters that would break out of HTML attributes or unquoted CSS url() values. +pub(crate) fn in_data_set(byte: u8) -> bool { + !matches!(byte, 0x21..=0x7E) + || matches!( + byte, + b'"' | b'#' | b'%' | b'\'' | b'(' | b')' | b'<' | b'>' | b'\\' | b'`' + ) +} + +/// Bytes that get percent-encoded in a fragment ("%" is kept as is, to preserve existing escapes). +pub(crate) fn in_fragment_set(byte: u8) -> bool { + !matches!(byte, 0x21..=0x7E) || matches!(byte, b'"' | b'<' | b'>' | b'`') +} + +/// Encoding applied to the metadata part when parsing, to mirror what URL parsers do +/// to opaque paths: C0 controls and non-ASCII get escaped, everything else stays. +pub(crate) fn in_c0_control_set(byte: u8) -> bool { + !(0x20..=0x7E).contains(&byte) +} + +pub(crate) fn encode_into(output: &mut String, input: &[u8], should_encode: fn(u8) -> bool) { + output.reserve(input.len()); + + for &byte in input { + if should_encode(byte) { + output.push('%'); + output.push(HEX[(byte >> 4) as usize] as char); + output.push(HEX[(byte & 0x0F) as usize] as char); + } else { + output.push(byte as char); + } + } +} + +fn hex_value(byte: u8) -> Option { + (byte as char).to_digit(16).map(|d| d as u8) +} + +/// Malformed sequences (a "%" not followed by two hex digits) are left as they are. +pub(crate) fn decode(input: &[u8]) -> Vec { + let mut output: Vec = Vec::with_capacity(input.len()); + let mut i = 0; + + while i < input.len() { + if input[i] == b'%' && i + 2 < input.len() { + if let (Some(high), Some(low)) = (hex_value(input[i + 1]), hex_value(input[i + 2])) { + output.push(high << 4 | low); + i += 3; + continue; + } + } + output.push(input[i]); + i += 1; + } + + output +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn decodes_valid_and_keeps_malformed_sequences() { + assert_eq!(decode(b"%41%4a%zz%4%"), b"AJ%zz%4%"); + assert_eq!(decode(b"%FF"), [0xFF]); + } + + #[test] + fn encoding_round_trips() { + let data: Vec = (0..=255).collect(); + let mut s = String::new(); + encode_into(&mut s, &data, in_data_set); + assert!(s.is_ascii()); + assert_eq!(decode(s.as_bytes()), data); + } +} diff --git a/tests/cli.rs b/tests/cli.rs new file mode 100644 index 0000000..81db4ba --- /dev/null +++ b/tests/cli.rs @@ -0,0 +1,289 @@ +use std::io::Write; +use std::path::PathBuf; +use std::process::{Command, Output, Stdio}; + +fn data_file(name: &str) -> PathBuf { + [env!("CARGO_MANIFEST_DIR"), "tests", "_data_", name] + .iter() + .collect() +} + +fn run(args: &[&str], stdin: &[u8]) -> Output { + let mut child = Command::new(env!("CARGO_BIN_EXE_dataurl")) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + child.stdin.take().unwrap().write_all(stdin).unwrap(); + child.wait_with_output().unwrap() +} + +#[track_caller] +fn assert_success(output: &Output, expected_stdout: &[u8]) { + assert_eq!(String::from_utf8_lossy(&output.stderr), ""); + assert_eq!(output.stdout, expected_stdout); + assert!(output.status.success()); +} + +#[track_caller] +fn assert_failure(output: &Output, expected_stderr: &str) { + assert_eq!(String::from_utf8_lossy(&output.stderr), expected_stderr); + assert_eq!(output.stdout, b""); + assert!(!output.status.success()); +} + +mod basic { + use super::*; + + #[test] + fn help() { + let output = run(&["-h"], b""); + let stdout = String::from_utf8(output.stdout.clone()).unwrap(); + assert!(stdout.starts_with(concat!("dataurl ", env!("CARGO_PKG_VERSION"), "\n"))); + assert!(stdout.contains("Usage: dataurl [OPTIONS] [INPUT]")); + assert!(stdout.contains("--media-type ")); + assert_eq!(run(&["--help"], b"").stdout, output.stdout); + assert!(output.status.success()); + } + + #[test] + fn version() { + assert_success( + &run(&["-V"], b""), + concat!("dataurl ", env!("CARGO_PKG_VERSION"), "\n").as_bytes(), + ); + } + + #[test] + fn unknown_argument() { + let output = run(&["-X", ""], b""); + assert_failure( + &output, + "error: unexpected argument '-X'\n\nUsage: dataurl [OPTIONS] [INPUT]\n\nFor more information, try '--help'.\n", + ); + assert_eq!(output.status.code(), Some(2)); + } + + #[test] + fn missing_option_value() { + assert!(String::from_utf8_lossy(&run(&["-t"], b"").stderr) + .starts_with("error: a value is required for '--media-type'")); + } + + #[test] + fn two_positional_inputs() { + assert!(!run(&["a", "b"], b"").status.success()); + } + + #[test] + fn file_and_argument_inputs() { + let file = data_file("text-file.txt"); + assert_failure( + &run(&["-i", file.to_str().unwrap(), "text"], b""), + "error: Both file and argument inputs provided\n", + ); + } + + #[test] + fn unreadable_input_file() { + let output = run(&["-i", "does/not/exist"], b""); + assert!(String::from_utf8_lossy(&output.stderr) + .starts_with("error: Unable to read input file 'does/not/exist'")); + assert!(!output.status.success()); + } +} + +mod encode { + use super::*; + + #[test] + fn empty_argument() { + assert_success(&run(&[""], b""), b"data:,\n"); + } + + #[test] + fn basic_argument() { + assert_success(&run(&["Hello, world!"], b""), b"data:,Hello,%20world!\n"); + } + + #[test] + fn non_ascii_argument_gets_utf8_charset() { + assert_success(&run(&["Ü"], b""), b"data:;charset=UTF-8,%C3%9C\n"); + } + + #[test] + fn base64() { + assert_success(&run(&["-b", ""], b""), b"data:;base64,\n"); + assert_success(&run(&["-b", " "], b""), b"data:;base64,IA==\n"); + assert_success( + &run(&["--base64", "\u{fb}\u{ff}"], b""), + b"data:;charset=UTF-8;base64,w7vDvw==\n", + ); + } + + #[test] + fn media_type() { + assert_success( + &run(&["-b", "-t", "text/html", " "], b""), + b"data:text/html;base64,IA==\n", + ); + assert_success( + &run(&["--media-type=TEXT/HTML", " "], b""), + b"data:text/html,%20\n", + ); + assert_success(&run(&["-ttext/css", "a{}"], b""), b"data:text/css,a{}\n"); + } + + #[test] + fn invalid_media_type() { + assert_failure( + &run(&["-t", "wrong/media/type", "Ü"], b""), + "error: Invalid media type 'wrong/media/type'\n", + ); + } + + #[test] + fn charset() { + assert_success( + &run(&["-b", "-c", "utf8", " "], b""), + b"data:;charset=UTF-8;base64,IA==\n", + ); + } + + #[test] + fn charset_converts_argument_text() { + assert_success( + &run(&["-c", "gbk", "你好"], b""), + b"data:;charset=GBK,%C4%E3%BA%C3\n", + ); + } + + #[test] + fn charset_unable_to_represent_argument_text() { + assert_failure( + &run(&["-c", "gbk", "Ü"], b""), + "error: Input text can't be represented in GBK\n", + ); + } + + #[test] + fn charset_keeps_stdin_bytes_as_is() { + assert_success( + &run(&["-c", "gbk"], b"\xC4\xE3"), + b"data:;charset=GBK,%C4%E3\n", + ); + } + + #[test] + fn invalid_charset() { + assert_failure( + &run(&["-c", "BAD-CHARSET", "Ü"], b""), + "error: Invalid encoding 'BAD-CHARSET'\n", + ); + } + + #[test] + fn fragment() { + assert_success( + &run(&["-b", "-f", "something", " "], b""), + b"data:;base64,IA==#something\n", + ); + assert_success(&run(&["-b", "-f", "", " "], b""), b"data:;base64,IA==#\n"); + } + + #[test] + fn stdin() { + assert_success(&run(&[], b"from stdin"), b"data:,from%20stdin\n"); + assert_success(&run(&["-i", "-"], b"from stdin"), b"data:,from%20stdin\n"); + } + + #[test] + fn input_file() { + let file = data_file("text-file.txt"); + let contents = std::fs::read(&file).unwrap(); + let output = run(&["-i", file.to_str().unwrap()], b""); + assert!(output.status.success()); + let data_url = dataurl::DataUrl::parse(&String::from_utf8(output.stdout).unwrap()).unwrap(); + assert_eq!(data_url.data(), contents); + } + + #[test] + fn output_file() { + let path = std::env::temp_dir().join(format!("dataurl-test-{}.txt", std::process::id())); + assert_success(&run(&["-o", path.to_str().unwrap(), "x"], b""), b""); + assert_eq!(std::fs::read(&path).unwrap(), b"data:,x\n"); + std::fs::remove_file(path).unwrap(); + } +} + +mod decode { + use super::*; + + #[test] + fn empty_data_url() { + assert_success(&run(&["-d", "data:,"], b""), b""); + } + + #[test] + fn base64_png_round_trip() { + let file = data_file("lizard.png"); + let png = std::fs::read(&file).unwrap(); + + let encoded = run( + &["-b", "-t", "image/png", "-i", file.to_str().unwrap()], + b"", + ); + assert!(encoded.status.success()); + assert_success(&run(&["-d"], &encoded.stdout), &png); + } + + #[test] + fn gbk_data_url_outputs_raw_bytes_when_piped() { + let output = run( + &["-d", "data:;charset=gbk;base64,PbnjtqvKocnu29rK0LGmsLI="], + b"", + ); + assert_success( + &output, + &[ + 0x3D, 0xB9, 0xE3, 0xB6, 0xAB, 0xCA, 0xA1, 0xC9, 0xEE, 0xDB, 0xDA, 0xCA, 0xD0, 0xB1, + 0xA6, 0xB0, 0xB2, + ], + ); + } + + #[test] + fn stdin_with_trailing_newline() { + assert_success(&run(&["-d"], b"data:,piped%20in\n"), b"piped in"); + } + + #[test] + fn output_file() { + let path = + std::env::temp_dir().join(format!("dataurl-test-decode-{}.bin", std::process::id())); + assert_success( + &run( + &["-d", "-o", path.to_str().unwrap(), "data:;base64,+/8="], + b"", + ), + b"", + ); + assert_eq!(std::fs::read(&path).unwrap(), [0xFB, 0xFF]); + std::fs::remove_file(path).unwrap(); + } + + #[test] + fn empty_input() { + assert_failure(&run(&["-d", ""], b""), "error: not a data URL\n"); + } + + #[test] + fn invalid_base64() { + assert_failure( + &run(&["-d", "data:;base64,a"], b""), + "error: data URL contains invalid base64\n", + ); + } +} diff --git a/tests/cli/basic.rs b/tests/cli/basic.rs deleted file mode 100644 index 0c765d4..0000000 --- a/tests/cli/basic.rs +++ /dev/null @@ -1,128 +0,0 @@ -// ██████╗ █████╗ ███████╗███████╗██╗███╗ ██╗ ██████╗ -// ██╔══██╗██╔══██╗██╔════╝██╔════╝██║████╗ ██║██╔════╝ -// ██████╔╝███████║███████╗███████╗██║██╔██╗ ██║██║ ███╗ -// ██╔═══╝ ██╔══██║╚════██║╚════██║██║██║╚██╗██║██║ ██║ -// ██║ ██║ ██║███████║███████║██║██║ ╚████║╚██████╔╝ -// ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ - -#[cfg(test)] -mod passing { - use assert_cmd::prelude::*; - use std::process::Command; - - #[test] - fn must_print_help_information_out_when_asked_to() { - let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); - let assert = cmd.arg("-h").assert(); - - let help_message: String = format!( - "{bin} {ver} - -Sunshine -CLI tool and Rust crate for parsing and generating data URLs - -USAGE: - {bin}{exe} [FLAGS] [OPTIONS] [INPUT] - -FLAGS: - -b, --base64 Enforces base64 encoding - -d, --decode Toggles decode mode on - -h, --help Prints help information - -V, --version Prints version information - -OPTIONS: - -c, --charset Sets custom encoding parameter - -f, --fragment Appends URL fragment - -i, --input-file Provides input file - -t, --media-type Sets custom media type - -o, --output-file Specifies output file - -ARGS: - Input string -", - bin = env!("CARGO_PKG_NAME"), - ver = env!("CARGO_PKG_VERSION"), - exe = if cfg!(windows) { ".exe" } else { "" } - ); - - assert - // Exit code must be 0 - .success() - // STDERR must be empty - .stderr("") - // STDOUT must contain program name, version, and usage information - .stdout(help_message); - } - - #[test] - fn must_print_program_name_and_version_number_when_asked_to() { - let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); - let assert = cmd.arg("-V").assert(); - - assert - // Exit code must be 0 - .success() - // STDERR must be empty - .stderr("") - // STDOUT must contain program name and version - .stdout(format!( - "{bin} {ver}\n", - bin = env!("CARGO_PKG_NAME"), - ver = env!("CARGO_PKG_VERSION") - )); - } -} - -// ███████╗ █████╗ ██╗██╗ ██╗███╗ ██╗ ██████╗ -// ██╔════╝██╔══██╗██║██║ ██║████╗ ██║██╔════╝ -// █████╗ ███████║██║██║ ██║██╔██╗ ██║██║ ███╗ -// ██╔══╝ ██╔══██║██║██║ ██║██║╚██╗██║██║ ██║ -// ██║ ██║ ██║██║███████╗██║██║ ╚████║╚██████╔╝ -// ╚═╝ ╚═╝ ╚═╝╚═╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ - -#[cfg(test)] -mod failing { - use assert_cmd::prelude::*; - use std::process::Command; - - #[test] - fn must_fail_when_given_wrong_argument() { - let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); - let assert = cmd.arg("-X").arg("").assert(); - - assert - // Exit code must be 1 - .failure() - // STDERR must contain error message - .stderr(format!( - "error: Found argument '-X' which wasn't expected, or isn't valid in this context - -USAGE: - {bin}{exe} [FLAGS] [OPTIONS] [INPUT] - -For more information try --help\n", - bin = env!("CARGO_PKG_NAME"), - exe = if cfg!(windows) { ".exe" } else { "" } - )) - // STDOUT must contain absolutely nothing - .stdout(""); - } - - #[test] - fn must_fail_when_both_file_and_argument_input_given() { - let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); - let assert = cmd - .arg("-i") - .arg("_data_/text-file.txt") - .arg("text") - .assert(); - - assert - // Exit code must be 1 - .failure() - // STDERR must contain error message - .stderr("error: Both file and argument inputs provided\n") - // STDOUT must contain absolutely nothing - .stdout(""); - } -} diff --git a/tests/cli/decode.rs b/tests/cli/decode.rs deleted file mode 100644 index ec8009f..0000000 --- a/tests/cli/decode.rs +++ /dev/null @@ -1,76 +0,0 @@ -// ██████╗ █████╗ ███████╗███████╗██╗███╗ ██╗ ██████╗ -// ██╔══██╗██╔══██╗██╔════╝██╔════╝██║████╗ ██║██╔════╝ -// ██████╔╝███████║███████╗███████╗██║██╔██╗ ██║██║ ███╗ -// ██╔═══╝ ██╔══██║╚════██║╚════██║██║██║╚██╗██║██║ ██║ -// ██║ ██║ ██║███████║███████║██║██║ ╚████║╚██████╔╝ -// ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ - -#[cfg(test)] -mod passing { - use assert_cmd::prelude::*; - use std::process::Command; - - #[test] - fn must_parse_empty_data_url_arg_input_and_output_nothing_into_stdout() { - let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); - let assert = cmd.arg("-d").arg("data:,").assert(); - - assert - // Exit code must be 0 - .success() - // STDERR must be empty - .stderr("") - // STDOUT must be empty - .stdout(""); - } - - #[test] - fn must_properly_parse_and_output_gbk_encoded_data_urls() { - let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); - let out = cmd - .arg("-d") - .arg("data:;charset=gbk;base64,PbnjtqvKocnu29rK0LGmsLI=") - .output() - .unwrap(); - - // STDOUT must contain GBK characters - assert_eq!( - &out.stdout, - &[61, 185, 227, 182, 171, 202, 161, 201, 238, 219, 218, 202, 208, 177, 166, 176, 178] - ); - - // STDERR must be empty - assert_eq!(&out.stderr, &[]); - - // Exit code must be 0 - out.assert().code(0); - } -} - -// ███████╗ █████╗ ██╗██╗ ██╗███╗ ██╗ ██████╗ -// ██╔════╝██╔══██╗██║██║ ██║████╗ ██║██╔════╝ -// █████╗ ███████║██║██║ ██║██╔██╗ ██║██║ ███╗ -// ██╔══╝ ██╔══██║██║██║ ██║██║╚██╗██║██║ ██║ -// ██║ ██║ ██║██║███████╗██║██║ ╚████║╚██████╔╝ -// ╚═╝ ╚═╝ ╚═╝╚═╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ - -#[cfg(test)] -mod failing { - use assert_cmd::prelude::*; - use std::process::Command; - - #[test] - fn must_fail_if_given_empty_arg_input_and_output_error_message_into_stderr_and_nothing_into_stdout( - ) { - let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); - let assert = cmd.arg("-d").arg("").assert(); - - assert - // Exit code must be 1 - .failure() - // STDERR must contain error message - .stderr("error: DataUrlParseError\n") - // STDOUT must be empty - .stdout(""); - } -} diff --git a/tests/cli/encode.rs b/tests/cli/encode.rs deleted file mode 100644 index db121f9..0000000 --- a/tests/cli/encode.rs +++ /dev/null @@ -1,180 +0,0 @@ -// ██████╗ █████╗ ███████╗███████╗██╗███╗ ██╗ ██████╗ -// ██╔══██╗██╔══██╗██╔════╝██╔════╝██║████╗ ██║██╔════╝ -// ██████╔╝███████║███████╗███████╗██║██╔██╗ ██║██║ ███╗ -// ██╔═══╝ ██╔══██║╚════██║╚════██║██║██║╚██╗██║██║ ██║ -// ██║ ██║ ██║███████║███████║██║██║ ╚████║╚██████╔╝ -// ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ - -#[cfg(test)] -mod passing { - use assert_cmd::prelude::*; - use std::process::Command; - - #[test] - fn must_generate_empty_data_url_when_empty_arg_input_is_given() { - let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); - let assert = cmd.arg("").assert(); - - assert - // Exit code must be 0 - .success() - // STDERR must be empty - .stderr("") - // STDOUT must contain generated data URL - .stdout("data:,\n"); - } - - #[test] - fn must_generate_data_url_when_basic_arg_input_is_given() { - let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); - let assert = cmd.arg("Hello, world!").assert(); - - assert - // Exit code must be 0 - .success() - // STDERR must be empty - .stderr("") - // STDOUT must contain generated data URL - .stdout("data:,Hello%2C%20world%21\n"); - } - - #[test] - fn must_generate_empty_base64_encoded_data_url_when_b_flag_and_empty_arg_input_are_given() { - let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); - let assert = cmd.arg("-b").arg("").assert(); - - assert - // Exit code must be 0 - .success() - // STDERR must be empty - .stderr("") - // STDOUT must contain generated data URL - .stdout("data:;base64,\n"); - } - - #[test] - fn must_generate_short_base64_encoded_data_url_when_b_flag_and_whitespace_arg_input_are_given() - { - let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); - let assert = cmd.arg("-b").arg(" ").assert(); - - assert - // Exit code must be 0 - .success() - // STDERR must be empty - .stderr("") - // STDOUT must contain generated data URL - .stdout("data:;base64,IA==\n"); - } - - #[test] - fn must_support_setting_media_type() { - let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); - let assert = cmd.arg("-b").arg("-t").arg("text/html").arg(" ").assert(); - - assert - // Exit code must be 0 - .success() - // STDERR must be empty - .stderr("") - // STDOUT must contain generated data URL - .stdout("data:text/html;base64,IA==\n"); - } - - #[test] - fn must_support_setting_charset() { - let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); - let assert = cmd.arg("-b").arg("-c").arg("utf8").arg(" ").assert(); - - assert - // Exit code must be 0 - .success() - // STDERR must be empty - .stderr("") - // STDOUT must contain generated data URL - .stdout("data:;charset=UTF-8;base64,IA==\n"); - } - - #[test] - fn must_set_fragment_if_provided() { - let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); - let assert = cmd.arg("-b").arg("-f").arg("something").arg(" ").assert(); - - assert - // Exit code must be 0 - .success() - // STDERR must be empty - .stderr("") - // STDOUT must contain generated data URL - .stdout("data:;base64,IA==#something\n"); - } - - #[test] - fn must_set_empty_fragment_if_provided() { - let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); - let assert = cmd.arg("-b").arg("-f").arg("").arg(" ").assert(); - - assert - // Exit code must be 0 - .success() - // STDERR must be empty - .stderr("") - // STDOUT must contain generated data URL - .stdout("data:;base64,IA==#\n"); - } - - #[test] - fn must_support_gbk_encoded_data_urls() { - let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); - let assert = cmd.arg("-c").arg("gbk").arg("Ü").assert(); - - assert - // Exit code must be 0 - .success() - // STDERR must be completely empty - .stderr("") - // STDOUT must contain properly encoded data URL - .stdout("data:;charset=GBK,%26%23220%3B\n"); - } -} - -// ███████╗ █████╗ ██╗██╗ ██╗███╗ ██╗ ██████╗ -// ██╔════╝██╔══██╗██║██║ ██║████╗ ██║██╔════╝ -// █████╗ ███████║██║██║ ██║██╔██╗ ██║██║ ███╗ -// ██╔══╝ ██╔══██║██║██║ ██║██║╚██╗██║██║ ██║ -// ██║ ██║ ██║██║███████╗██║██║ ╚████║╚██████╔╝ -// ╚═╝ ╚═╝ ╚═╝╚═╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ - -#[cfg(test)] -mod failing { - use assert_cmd::prelude::*; - use std::process::Command; - - #[test] - fn must_not_allow_incorrect_media_type_to_be_set() { - let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); - let assert = cmd.arg("-t").arg("wrong/media/type").arg("Ü").assert(); - - assert - // Exit code must be 1 - .failure() - // STDERR must contain error message - .stderr("error: Invalid media type 'wrong/media/type'\n") - // STDOUT must be empty - .stdout(""); - } - - #[test] - fn must_not_allow_incorrect_charset_to_be_set() { - let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); - let assert = cmd.arg("-c").arg("BAD-CHARSET").arg("Ü").assert(); - - assert - // Exit code must be 1 - .failure() - // STDERR must contain error message - .stderr("error: Invalid encoding 'BAD-CHARSET'\n") - // STDOUT must be empty - .stdout(""); - } -} diff --git a/tests/cli/mod.rs b/tests/cli/mod.rs deleted file mode 100644 index 030b83a..0000000 --- a/tests/cli/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod basic; -mod decode; -mod encode; diff --git a/tests/common/json.rs b/tests/common/json.rs new file mode 100644 index 0000000..6536b3a --- /dev/null +++ b/tests/common/json.rs @@ -0,0 +1,205 @@ +//! Minimal JSON reader for the Web Platform Tests fixtures, kept dependency-free on purpose. +//! It panics on malformed input, which is fine for test data we control. + +#![allow(dead_code)] + +pub enum Json { + Null, + Bool(bool), + Number(f64), + String(String), + Array(Vec), + Object(Vec<(String, Json)>), +} + +impl Json { + pub fn as_array(&self) -> &[Json] { + match self { + Json::Array(items) => items, + _ => panic!("expected a JSON array"), + } + } + + pub fn as_str(&self) -> &str { + self.as_opt_str().expect("expected a JSON string") + } + + pub fn as_opt_str(&self) -> Option<&str> { + match self { + Json::String(s) => Some(s), + Json::Null => None, + _ => panic!("expected a JSON string or null"), + } + } + + pub fn as_bytes(&self) -> Vec { + self.as_array() + .iter() + .map(|item| match item { + Json::Number(n) if (0.0..=255.0).contains(n) => *n as u8, + _ => panic!("expected an array of bytes"), + }) + .collect() + } + + pub fn is_null(&self) -> bool { + matches!(self, Json::Null) + } + + pub fn get_opt(&self, key: &str) -> Option<&Json> { + match self { + Json::Object(members) => members.iter().find(|(k, _)| k == key).map(|(_, v)| v), + _ => panic!("expected a JSON object"), + } + } + + pub fn get(&self, key: &str) -> &Json { + self.get_opt(key) + .unwrap_or_else(|| panic!("missing key {key:?}")) + } +} + +pub fn parse(input: &str) -> Json { + let mut parser = Parser { + chars: input.chars().collect(), + pos: 0, + }; + let value = parser.value(); + parser.skip_whitespace(); + assert_eq!( + parser.pos, + parser.chars.len(), + "trailing data after JSON value" + ); + value +} + +struct Parser { + chars: Vec, + pos: usize, +} + +impl Parser { + fn skip_whitespace(&mut self) { + while self.pos < self.chars.len() && self.chars[self.pos].is_whitespace() { + self.pos += 1; + } + } + + fn next(&mut self) -> char { + let c = *self.chars.get(self.pos).expect("unexpected end of JSON"); + self.pos += 1; + c + } + + fn expect_literal(&mut self, literal: &str) { + for expected in literal.chars() { + assert_eq!(self.next(), expected, "invalid JSON literal"); + } + } + + fn value(&mut self) -> Json { + self.skip_whitespace(); + match self.chars[self.pos] { + 'n' => { + self.expect_literal("null"); + Json::Null + } + 't' => { + self.expect_literal("true"); + Json::Bool(true) + } + 'f' => { + self.expect_literal("false"); + Json::Bool(false) + } + '"' => Json::String(self.string()), + '[' => { + self.pos += 1; + let mut items = Vec::new(); + loop { + self.skip_whitespace(); + if self.chars[self.pos] == ']' { + self.pos += 1; + break; + } + items.push(self.value()); + self.skip_whitespace(); + if self.next() == ']' { + break; + } + } + Json::Array(items) + } + '{' => { + self.pos += 1; + let mut members = Vec::new(); + loop { + self.skip_whitespace(); + if self.chars[self.pos] == '}' { + self.pos += 1; + break; + } + let key = self.string(); + self.skip_whitespace(); + assert_eq!(self.next(), ':'); + members.push((key, self.value())); + self.skip_whitespace(); + if self.next() == '}' { + break; + } + } + Json::Object(members) + } + _ => { + let start = self.pos; + while self.pos < self.chars.len() + && matches!( + self.chars[self.pos], + '-' | '+' | '.' | 'e' | 'E' | '0'..='9' + ) + { + self.pos += 1; + } + let number: String = self.chars[start..self.pos].iter().collect(); + Json::Number(number.parse().expect("invalid JSON number")) + } + } + } + + fn hex4(&mut self) -> u32 { + let digits: String = (0..4).map(|_| self.next()).collect(); + u32::from_str_radix(&digits, 16).expect("invalid \\u escape") + } + + fn string(&mut self) -> String { + assert_eq!(self.next(), '"'); + let mut s = String::new(); + loop { + match self.next() { + '"' => break, + '\\' => match self.next() { + 'n' => s.push('\n'), + 't' => s.push('\t'), + 'r' => s.push('\r'), + 'b' => s.push('\u{8}'), + 'f' => s.push('\u{c}'), + 'u' => { + let mut code = self.hex4(); + if (0xD800..0xDC00).contains(&code) { + // Surrogate pair + assert_eq!(self.next(), '\\'); + assert_eq!(self.next(), 'u'); + let low = self.hex4(); + code = 0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00); + } + s.push(char::from_u32(code).expect("invalid code point")); + } + c => s.push(c), // \" \\ \/ + }, + c => s.push(c), + } + } + s + } +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..22fdbb3 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1 @@ +pub mod json; diff --git a/tests/lib.rs b/tests/lib.rs new file mode 100644 index 0000000..3c3183b --- /dev/null +++ b/tests/lib.rs @@ -0,0 +1,409 @@ +use dataurl::{DataUrl, Error}; + +const LIZARD_PNG: &[u8] = include_bytes!("_data_/lizard.png"); + +mod parse { + use super::*; + + #[test] + fn empty() -> Result<(), Error> { + let data_url = DataUrl::parse("data:,")?; + assert_eq!(data_url.media_type(), "text/plain"); + assert_eq!(data_url.media_type_no_default(), None); + assert_eq!(data_url.mime_type(), "text/plain;charset=US-ASCII"); + assert_eq!(data_url.charset(), "US-ASCII"); + assert_eq!(data_url.charset_no_default(), None); + assert!(!data_url.is_base64_encoded()); + assert_eq!(data_url.data(), b""); + assert_eq!(data_url.fragment(), None); + Ok(()) + } + + #[test] + fn base64_with_plus_and_slash() -> Result<(), Error> { + // The standard base64 alphabet uses "+" and "/" (0.1.x expected "-" and "_" instead) + let data_url = DataUrl::parse("data:application/octet-stream;base64,+/8=")?; + assert_eq!(data_url.data(), [0xFB, 0xFF]); + assert!(data_url.is_base64_encoded()); + Ok(()) + } + + #[test] + fn base64_without_padding_and_with_whitespace() -> Result<(), Error> { + let data_url = DataUrl::parse("data:;base64,SGVs bG8\n")?; + assert_eq!(data_url.data(), b"Hello"); + Ok(()) + } + + #[test] + fn ignores_tabs_and_newlines_anywhere() -> Result<(), Error> { + let data_url = DataUrl::parse(" data:image/png;\nbase64,\n\tiVBO\r\nRw==\n")?; + assert_eq!(data_url.media_type(), "image/png"); + assert_eq!(data_url.data(), [0x89, 0x50, 0x4E, 0x47]); + Ok(()) + } + + #[test] + fn scheme_and_media_type_are_case_insensitive() -> Result<(), Error> { + let data_url = DataUrl::parse("DATA:Text/HTML;CharSet=UTF-8,x")?; + assert_eq!(data_url.media_type(), "text/html"); + assert_eq!(data_url.charset(), "UTF-8"); + Ok(()) + } + + #[test] + fn charset_after_whitespace() -> Result<(), Error> { + let data_url = DataUrl::parse("data:text/html; charset=utf-8,x")?; + assert_eq!(data_url.charset(), "utf-8"); + assert_eq!(data_url.mime_type(), "text/html;charset=utf-8"); + Ok(()) + } + + #[test] + fn quoted_charset() -> Result<(), Error> { + let data_url = DataUrl::parse("data:text/plain;charset=\"utf-8\",x")?; + assert_eq!(data_url.charset(), "utf-8"); + Ok(()) + } + + #[test] + fn keeps_other_parameters() -> Result<(), Error> { + let data_url = DataUrl::parse("data:text/plain;format=flowed;charset=utf-8,x")?; + assert_eq!( + data_url.parameters().collect::>(), + [("format", "flowed"), ("charset", "utf-8")] + ); + assert_eq!(data_url.parameter("FORMAT"), Some("flowed")); + Ok(()) + } + + #[test] + fn many_parameters() -> Result<(), Error> { + // 0.1.x used an i8 counter here, which overflowed + let input = format!("data:text/plain{},x", ";a".repeat(300)); + assert_eq!(DataUrl::parse(&input)?.media_type(), "text/plain"); + Ok(()) + } + + #[test] + fn percent_encoded_binary_data() -> Result<(), Error> { + let data_url = DataUrl::parse("data:image/png,%89PNG%0D%0A%1A%0A")?; + assert_eq!(data_url.data(), b"\x89PNG\r\n\x1a\n"); + Ok(()) + } + + #[test] + fn non_ascii_data_becomes_utf8() -> Result<(), Error> { + let data_url = DataUrl::parse("data:,Привет")?; + assert_eq!(data_url.data(), "Привет".as_bytes()); + Ok(()) + } + + #[test] + fn query_is_part_of_data() -> Result<(), Error> { + let data_url = DataUrl::parse("data:text/html,textok?a=v#f")?; + assert_eq!(data_url.data(), b"textok?a=v"); + assert_eq!(data_url.fragment(), Some("f")); + Ok(()) + } + + #[test] + fn empty_fragment() -> Result<(), Error> { + assert_eq!(DataUrl::parse("data:,x#")?.fragment(), Some("")); + Ok(()) + } + + #[test] + fn hierarchical_form() -> Result<(), Error> { + assert_eq!(DataUrl::parse("data://host/a/../,X")?.data(), b"X"); + assert_eq!(DataUrl::parse("data:/x/./,X")?.data(), b"X"); + assert_eq!(DataUrl::parse("data://[::1]:80/,X")?.data(), b"X"); + assert_eq!( + DataUrl::parse("data://host:port/,X"), + Err(Error::InvalidUrl) + ); + assert_eq!(DataUrl::parse("data://:80/,X"), Err(Error::InvalidUrl)); + Ok(()) + } + + #[test] + fn errors() { + assert_eq!(DataUrl::parse(""), Err(Error::NotADataUrl)); + assert_eq!(DataUrl::parse("http://a/,b"), Err(Error::NotADataUrl)); + assert_eq!(DataUrl::parse("dat"), Err(Error::NotADataUrl)); + assert_eq!(DataUrl::parse("data:text/html"), Err(Error::MissingComma)); + assert_eq!( + DataUrl::parse("data:,x#a,b").map(|d| d.data().to_vec()), + Ok(b"x".to_vec()) + ); + assert_eq!(DataUrl::parse("data:#,x"), Err(Error::MissingComma)); + assert_eq!(DataUrl::parse("data:;base64,a"), Err(Error::InvalidBase64)); + assert_eq!( + DataUrl::parse("data:;base64,-_8="), + Err(Error::InvalidBase64) + ); + } + + #[test] + fn from_str_and_try_from() -> Result<(), Error> { + let a: DataUrl = "data:,x".parse()?; + let b = DataUrl::try_from("data:,x")?; + assert_eq!(a, b); + Ok(()) + } +} + +mod serialize { + use super::*; + + #[test] + fn empty() { + let mut data_url = DataUrl::new(); + assert_eq!(data_url.to_string(), "data:,"); + data_url.set_is_base64_encoded(true); + assert_eq!(data_url.to_string(), "data:;base64,"); + } + + #[test] + fn readable_percent_encoding() { + let mut data_url = DataUrl::new(); + data_url.set_data(b"Hello, World! 100% (\"#\")"); + assert_eq!( + data_url.to_string(), + "data:,Hello,%20World!%20100%25%20%3Cok%3E%20%28%22%23%22%29" + ); + } + + #[test] + fn base64_uses_standard_alphabet() { + let mut data_url = DataUrl::new(); + data_url.set_media_type(Some("image/png")).unwrap(); + data_url.set_is_base64_encoded(true); + data_url.set_data(&[0xFB, 0xFF]); + assert_eq!(data_url.to_string(), "data:image/png;base64,+/8="); + } + + #[test] + fn data_is_never_transcoded() { + // windows-1252 bytes for "café", which 0.1.x mangled into U+FFFD + let mut data_url = DataUrl::new(); + data_url.set_charset(Some("windows-1252")).unwrap(); + data_url.set_data(b"caf\xE9"); + assert_eq!(data_url.to_string(), "data:;charset=windows-1252,caf%E9"); + assert_eq!(data_url.text(), "café"); + } + + #[test] + fn parameters_without_media_type() { + let mut data_url = DataUrl::new(); + data_url.set_charset(Some("utf8")).unwrap(); + assert_eq!(data_url.to_string(), "data:;charset=UTF-8,"); + assert_eq!(data_url.mime_type(), "text/plain;charset=UTF-8"); + } + + #[test] + fn quoted_parameter_values() { + let mut data_url = DataUrl::new(); + data_url.set_media_type(Some("text/plain")).unwrap(); + data_url.set_parameter("name", Some("a \"b\";c")).unwrap(); + let serialized = data_url.to_string(); + assert_eq!(serialized, "data:text/plain;name=\"a \\\"b\\\";c\","); + assert_eq!( + DataUrl::parse(&serialized).unwrap().parameter("name"), + Some("a \"b\";c") + ); + } + + #[test] + fn fragment() { + let mut data_url = DataUrl::new(); + data_url.set_fragment(Some("some thing")); + assert_eq!(data_url.to_string(), "data:,#some%20thing"); + data_url.set_fragment(Some("")); + assert_eq!(data_url.to_string(), "data:,#"); + data_url.set_fragment(None); + assert_eq!(data_url.to_string(), "data:,"); + } + + #[test] + fn round_trips_binary_file() -> Result<(), Error> { + for base64 in [true, false] { + let mut data_url = DataUrl::new(); + data_url.set_media_type(Some("image/png"))?; + data_url.set_is_base64_encoded(base64); + data_url.set_data(LIZARD_PNG); + + let parsed = DataUrl::parse(&data_url.to_string())?; + assert_eq!(parsed, data_url); + assert_eq!(parsed.into_data(), LIZARD_PNG); + } + Ok(()) + } +} + +mod setters { + use super::*; + + #[test] + fn media_type() { + let mut data_url = DataUrl::new(); + assert_eq!(data_url.set_media_type(Some(" Image/SVG+XML ")), Ok(())); + assert_eq!(data_url.media_type(), "image/svg+xml"); + assert_eq!(data_url.set_media_type(None), Ok(())); + assert_eq!(data_url.media_type_no_default(), None); + + for invalid in [ + "", + "text", + "text/", + "/html", + "wrong/media/type", + "text/html;charset=utf-8", + "te xt/html", + ] { + assert_eq!( + data_url.set_media_type(Some(invalid)), + Err(Error::InvalidMediaType), + "{invalid:?}" + ); + } + } + + #[test] + fn media_type_keeps_charset() { + let mut data_url = DataUrl::new(); + data_url.set_charset(Some("utf-8")).unwrap(); + data_url.set_media_type(Some("text/html")).unwrap(); + assert_eq!(data_url.mime_type(), "text/html;charset=UTF-8"); + } + + #[test] + fn charset() { + let mut data_url = DataUrl::new(); + assert_eq!(data_url.set_charset(Some("utf8")), Ok(())); + assert_eq!(data_url.charset(), "UTF-8"); + assert_eq!(data_url.set_charset(Some("gb2312")), Ok(())); + assert_eq!(data_url.charset(), "GBK"); + assert_eq!(data_url.parameters().count(), 1); + assert_eq!( + data_url.set_charset(Some("BAD-CHARSET")), + Err(Error::InvalidCharset) + ); + assert_eq!( + data_url.set_charset(Some("iso-2022-kr")), + Err(Error::InvalidCharset) + ); + assert_eq!(data_url.charset(), "GBK"); + assert_eq!(data_url.set_charset(None), Ok(())); + assert_eq!(data_url.charset_no_default(), None); + } + + #[test] + fn parameter() { + let mut data_url = DataUrl::new(); + assert_eq!(data_url.set_parameter("Name", Some("value")), Ok(())); + assert_eq!(data_url.parameter("name"), Some("value")); + assert_eq!(data_url.set_parameter("name", Some("other")), Ok(())); + assert_eq!( + data_url.parameters().collect::>(), + [("name", "other")] + ); + assert_eq!(data_url.set_parameter("name", None), Ok(())); + assert_eq!(data_url.parameters().count(), 0); + + assert_eq!( + data_url.set_parameter("", Some("x")), + Err(Error::InvalidParameter) + ); + assert_eq!( + data_url.set_parameter("a b", Some("x")), + Err(Error::InvalidParameter) + ); + for invalid in ["a,b", "a#b", "é", "\n"] { + assert_eq!( + data_url.set_parameter("x", Some(invalid)), + Err(Error::InvalidParameter) + ); + } + } + + #[test] + fn text() -> Result<(), Error> { + let mut data_url = DataUrl::new(); + + data_url.set_text("café")?; // Default (US-ASCII, i.e. windows-1252) + assert_eq!(data_url.data(), b"caf\xE9"); + assert_eq!(data_url.set_text("☀"), Err(Error::UnencodableText)); + assert_eq!(data_url.data(), b"caf\xE9"); + + data_url.set_charset(Some("UTF-8"))?; + data_url.set_text("☀")?; + assert_eq!(data_url.data(), "☀".as_bytes()); + + data_url.set_charset(Some("utf-16le"))?; + data_url.set_text("A☀")?; + assert_eq!(data_url.data(), [0x41, 0x00, 0x00, 0x26]); + assert_eq!(data_url.text(), "A☀"); + + data_url.set_charset(Some("gbk"))?; + data_url.set_text("你好")?; + assert_eq!(data_url.data(), [0xC4, 0xE3, 0xBA, 0xC3]); + assert_eq!(data_url.text(), "你好"); + Ok(()) + } +} + +mod misc { + use super::*; + + #[test] + fn decodes_text_in_declared_charset() -> Result<(), Error> { + let data_url = DataUrl::parse("data:;charset=gbk;base64,PbnjtqvKocnu29rK0LGmsLI=")?; + assert_eq!(data_url.text(), "=广东省深圳市宝安"); + assert_eq!(data_url.encoding(), encoding_rs::GBK); + Ok(()) + } + + #[test] + fn unknown_charset_decodes_as_windows_1252() -> Result<(), Error> { + let data_url = DataUrl::parse("data:;charset=whatever,%E9")?; + assert_eq!(data_url.charset(), "whatever"); + assert_eq!(data_url.text(), "é"); + Ok(()) + } + + #[test] + fn is_binary() -> Result<(), Error> { + for (input, binary) in [ + ("data:,", false), + ("data:text/html,", false), + ("data:application/json,", false), + ("data:image/svg+xml,", false), + ("data:application/ld+json,", false), + ("data:image/png,", true), + ("data:application/octet-stream,", true), + ] { + assert_eq!(DataUrl::parse(input)?.is_binary(), binary, "{input}"); + } + Ok(()) + } + + #[test] + fn error_is_a_std_error() { + let result: Result> = + "data:;base64,a".parse::().map_err(Into::into); + assert_eq!( + result.unwrap_err().to_string(), + "data URL contains invalid base64" + ); + } + + #[test] + fn debug_does_not_dump_data() { + let mut data_url = DataUrl::new(); + data_url.set_data(LIZARD_PNG); + let debug = format!("{data_url:?}"); + assert!(debug.contains("[14196 bytes]"), "{debug}"); + assert!(debug.len() < 200); + } +} diff --git a/tests/lib/_misc_.rs b/tests/lib/_misc_.rs deleted file mode 100644 index 791de24..0000000 --- a/tests/lib/_misc_.rs +++ /dev/null @@ -1,31 +0,0 @@ -// ██████╗ █████╗ ███████╗███████╗██╗███╗ ██╗ ██████╗ -// ██╔══██╗██╔══██╗██╔════╝██╔════╝██║████╗ ██║██╔════╝ -// ██████╔╝███████║███████╗███████╗██║██╔██╗ ██║██║ ███╗ -// ██╔═══╝ ██╔══██║╚════██║╚════██║██║██║╚██╗██║██║ ██║ -// ██║ ██║ ██║███████║███████║██║██║ ╚████║╚██████╔╝ -// ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ - -#[cfg(test)] -mod passing { - use dataurl::{DataUrl, DataUrlParseError}; - - #[test] - fn must_have_correct_code_for_the_readme_usage_snippet() -> Result<(), DataUrlParseError> { - let data_url: DataUrl = DataUrl::parse("data:,Hello,%20World!")?; - - assert_eq!(data_url.media_type(), "text/plain".to_string()); - assert_eq!(data_url.media_type_no_default(), None); - assert_eq!(data_url.charset(), "US-ASCII".to_string()); - assert_eq!(data_url.charset_no_default(), None); - assert!(!data_url.is_base64_encoded()); - assert_eq!( - data_url.data(), - [72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33] - ); - assert_eq!(data_url.fragment(), None); - assert_eq!(data_url.to_string(), "data:,Hello%2C%20World%21"); - assert_eq!(data_url.text(), "Hello, World!"); - - Ok(()) - } -} diff --git a/tests/lib/charset.rs b/tests/lib/charset.rs deleted file mode 100644 index d1a570f..0000000 --- a/tests/lib/charset.rs +++ /dev/null @@ -1,55 +0,0 @@ -// ██████╗ █████╗ ███████╗███████╗██╗███╗ ██╗ ██████╗ -// ██╔══██╗██╔══██╗██╔════╝██╔════╝██║████╗ ██║██╔════╝ -// ██████╔╝███████║███████╗███████╗██║██╔██╗ ██║██║ ███╗ -// ██╔═══╝ ██╔══██║╚════██║╚════██║██║██║╚██╗██║██║ ██║ -// ██║ ██║ ██║███████║███████║██║██║ ╚████║╚██████╔╝ -// ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ - -#[cfg(test)] -mod passing { - use dataurl::{DataUrl, DataUrlParseError}; - - #[test] - fn must_be_us_ascii_by_default() -> Result<(), DataUrlParseError> { - let data_url = DataUrl::new(); - - assert_eq!(data_url.charset(), "US-ASCII"); - - Ok(()) - } - - #[test] - fn must_be_possible_to_set_to_utf8() -> Result<(), DataUrlParseError> { - let mut data_url = DataUrl::new(); - - assert!(data_url.set_charset(Some("utf8".to_string()))); - - assert_eq!(data_url.charset(), "UTF-8"); - - Ok(()) - } -} - -// ███████╗ █████╗ ██╗██╗ ██╗███╗ ██╗ ██████╗ -// ██╔════╝██╔══██╗██║██║ ██║████╗ ██║██╔════╝ -// █████╗ ███████║██║██║ ██║██╔██╗ ██║██║ ███╗ -// ██╔══╝ ██╔══██║██║██║ ██║██║╚██╗██║██║ ██║ -// ██║ ██║ ██║██║███████╗██║██║ ╚████║╚██████╔╝ -// ╚═╝ ╚═╝ ╚═╝╚═╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ - -#[cfg(test)] -mod failing { - use dataurl::{DataUrl, DataUrlParseError}; - - #[test] - fn must_fall_back_to_default_if_given_bad_charset() -> Result<(), DataUrlParseError> { - let mut data_url = DataUrl::new(); - - // This bad input must make it fall back to US-ASCII - assert!(!data_url.set_charset(Some("BAD-CHARSET".to_string()))); - - assert_eq!(data_url.charset(), "US-ASCII"); - - Ok(()) - } -} diff --git a/tests/lib/charset_no_default.rs b/tests/lib/charset_no_default.rs deleted file mode 100644 index 32a68bd..0000000 --- a/tests/lib/charset_no_default.rs +++ /dev/null @@ -1,55 +0,0 @@ -// ██████╗ █████╗ ███████╗███████╗██╗███╗ ██╗ ██████╗ -// ██╔══██╗██╔══██╗██╔════╝██╔════╝██║████╗ ██║██╔════╝ -// ██████╔╝███████║███████╗███████╗██║██╔██╗ ██║██║ ███╗ -// ██╔═══╝ ██╔══██║╚════██║╚════██║██║██║╚██╗██║██║ ██║ -// ██║ ██║ ██║███████║███████║██║██║ ╚████║╚██████╔╝ -// ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ - -#[cfg(test)] -mod passing { - use dataurl::{DataUrl, DataUrlParseError}; - - #[test] - fn must_be_us_none_by_default() -> Result<(), DataUrlParseError> { - let data_url = DataUrl::new(); - - assert_eq!(data_url.charset_no_default(), None); - - Ok(()) - } - - #[test] - fn must_be_possible_to_set_to_utf8() -> Result<(), DataUrlParseError> { - let mut data_url = DataUrl::new(); - - assert!(data_url.set_charset(Some("utf8".to_string()))); - - assert_eq!(data_url.charset_no_default(), Some("UTF-8".to_string())); - - Ok(()) - } -} - -// ███████╗ █████╗ ██╗██╗ ██╗███╗ ██╗ ██████╗ -// ██╔════╝██╔══██╗██║██║ ██║████╗ ██║██╔════╝ -// █████╗ ███████║██║██║ ██║██╔██╗ ██║██║ ███╗ -// ██╔══╝ ██╔══██║██║██║ ██║██║╚██╗██║██║ ██║ -// ██║ ██║ ██║██║███████╗██║██║ ╚████║╚██████╔╝ -// ╚═╝ ╚═╝ ╚═╝╚═╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ - -#[cfg(test)] -mod failing { - use dataurl::{DataUrl, DataUrlParseError}; - - #[test] - fn must_fall_back_to_none_if_given_bad_charset() -> Result<(), DataUrlParseError> { - let mut data_url = DataUrl::new(); - - // This bad input must make it fall back to US-ASCII - assert!(!data_url.set_charset(Some("BAD-CHARSET".to_string()))); - - assert_eq!(data_url.charset_no_default(), None); - - Ok(()) - } -} diff --git a/tests/lib/fragment.rs b/tests/lib/fragment.rs deleted file mode 100644 index 932461f..0000000 --- a/tests/lib/fragment.rs +++ /dev/null @@ -1,87 +0,0 @@ -// ██████╗ █████╗ ███████╗███████╗██╗███╗ ██╗ ██████╗ -// ██╔══██╗██╔══██╗██╔════╝██╔════╝██║████╗ ██║██╔════╝ -// ██████╔╝███████║███████╗███████╗██║██╔██╗ ██║██║ ███╗ -// ██╔═══╝ ██╔══██║╚════██║╚════██║██║██║╚██╗██║██║ ██║ -// ██║ ██║ ██║███████║███████║██║██║ ╚████║╚██████╔╝ -// ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ - -#[cfg(test)] -mod passing { - use dataurl::{DataUrl, DataUrlParseError}; - - #[test] - fn must_be_none_by_default() -> Result<(), DataUrlParseError> { - let data_url = DataUrl::new(); - - assert_eq!(data_url.fragment(), None); - - Ok(()) - } - - #[test] - fn must_be_rendered_as_pound_sign_if_set_to_an_empty_string() -> Result<(), DataUrlParseError> { - let mut data_url = DataUrl::new(); - - data_url.set_fragment(Some("".to_string())); - - assert_eq!(data_url.fragment(), Some("".to_string())); - - Ok(()) - } - - #[test] - fn must_parse_empty_string_if_just_pound_sign_given() -> Result<(), DataUrlParseError> { - let data_url = DataUrl::parse("data:,#")?; - - assert_eq!(data_url.fragment(), Some("".to_string())); - - Ok(()) - } - - #[test] - fn must_be_possible_to_unset() -> Result<(), DataUrlParseError> { - let mut data_url = DataUrl::parse("data:,#something")?; - - assert_eq!(data_url.fragment(), Some("something".to_string())); - - data_url.set_fragment(None); - - assert_eq!(data_url.fragment(), None); - - Ok(()) - } - - #[test] - - fn must_be_possible_to_set() -> Result<(), DataUrlParseError> { - let mut data_url = DataUrl::new(); - - data_url.set_fragment(Some("something".to_string())); - - assert_eq!(data_url.fragment(), Some("something".to_string())); - - Ok(()) - } - - #[test] - fn must_be_possible_to_set_it_to_a_whitespace() -> Result<(), DataUrlParseError> { - let mut data_url = DataUrl::new(); - - data_url.set_fragment(Some(" ".to_string())); - - assert_eq!(data_url.fragment(), Some(" ".to_string())); - - Ok(()) - } - - #[test] - fn must_remain_abset_if_given_none() -> Result<(), DataUrlParseError> { - let mut data_url = DataUrl::new(); - - data_url.set_fragment(None); - - assert_eq!(data_url.fragment(), None); - - Ok(()) - } -} diff --git a/tests/lib/media_type.rs b/tests/lib/media_type.rs deleted file mode 100644 index 47c68d0..0000000 --- a/tests/lib/media_type.rs +++ /dev/null @@ -1,77 +0,0 @@ -// ██████╗ █████╗ ███████╗███████╗██╗███╗ ██╗ ██████╗ -// ██╔══██╗██╔══██╗██╔════╝██╔════╝██║████╗ ██║██╔════╝ -// ██████╔╝███████║███████╗███████╗██║██╔██╗ ██║██║ ███╗ -// ██╔═══╝ ██╔══██║╚════██║╚════██║██║██║╚██╗██║██║ ██║ -// ██║ ██║ ██║███████║███████║██║██║ ╚████║╚██████╔╝ -// ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ - -#[cfg(test)] -mod passing { - use dataurl::{DataUrl, DataUrlParseError}; - - #[test] - fn must_be_text_plain_by_default() -> Result<(), DataUrlParseError> { - let data_url = DataUrl::new(); - - assert_eq!(data_url.media_type(), "text/plain"); - - Ok(()) - } - - #[test] - fn must_be_possible_to_set_to_image_png() -> Result<(), DataUrlParseError> { - let mut data_url = DataUrl::new(); - - data_url.set_media_type(Some("image/png".to_string())); - - assert_eq!(data_url.media_type(), "image/png"); - - Ok(()) - } -} - -// ███████╗ █████╗ ██╗██╗ ██╗███╗ ██╗ ██████╗ -// ██╔════╝██╔══██╗██║██║ ██║████╗ ██║██╔════╝ -// █████╗ ███████║██║██║ ██║██╔██╗ ██║██║ ███╗ -// ██╔══╝ ██╔══██║██║██║ ██║██║╚██╗██║██║ ██║ -// ██║ ██║ ██║██║███████╗██║██║ ╚████║╚██████╔╝ -// ╚═╝ ╚═╝ ╚═╝╚═╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ - -#[cfg(test)] -mod failing { - use dataurl::{DataUrl, DataUrlParseError}; - - #[test] - fn must_fall_back_to_text_plain_if_set_to_empty() -> Result<(), DataUrlParseError> { - let mut data_url = DataUrl::new(); - - data_url.set_media_type(Some("".to_string())); - - assert_eq!(data_url.media_type(), "text/plain"); - - Ok(()) - } - - #[test] - fn must_fall_back_to_text_plain_if_attempted_to_set_to_whitespace( - ) -> Result<(), DataUrlParseError> { - let mut data_url = DataUrl::new(); - - data_url.set_media_type(Some(" ".to_string())); - - assert_eq!(data_url.media_type(), "text/plain"); - - Ok(()) - } - - // #[test] - // fn must_fall_back_to_text_plain_if_attempted_to_set_to_bad() -> Result<(), DataUrlParseError> { - // let mut data_url = DataUrl::new(); - - // data_url.set_media_type(Some("bad".to_string())); - - // assert_eq!(data_url.media_type(), "text/plain"); - - // Ok(()) - // } -} diff --git a/tests/lib/media_type_no_default.rs b/tests/lib/media_type_no_default.rs deleted file mode 100644 index eb040e0..0000000 --- a/tests/lib/media_type_no_default.rs +++ /dev/null @@ -1,78 +0,0 @@ -// ██████╗ █████╗ ███████╗███████╗██╗███╗ ██╗ ██████╗ -// ██╔══██╗██╔══██╗██╔════╝██╔════╝██║████╗ ██║██╔════╝ -// ██████╔╝███████║███████╗███████╗██║██╔██╗ ██║██║ ███╗ -// ██╔═══╝ ██╔══██║╚════██║╚════██║██║██║╚██╗██║██║ ██║ -// ██║ ██║ ██║███████║███████║██║██║ ╚████║╚██████╔╝ -// ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ - -#[cfg(test)] -mod passing { - use dataurl::{DataUrl, DataUrlParseError}; - - #[test] - fn must_be_none_by_default() -> Result<(), DataUrlParseError> { - let data_url = DataUrl::new(); - - assert_eq!(data_url.media_type_no_default(), None); - - Ok(()) - } - - #[test] - fn must_be_possible_to_set_to_image_png() -> Result<(), DataUrlParseError> { - let mut data_url = DataUrl::new(); - - data_url.set_media_type(Some("image/png".to_string())); - - assert_eq!( - data_url.media_type_no_default(), - Some("image/png".to_string()) - ); - - Ok(()) - } -} - -// ███████╗ █████╗ ██╗██╗ ██╗███╗ ██╗ ██████╗ -// ██╔════╝██╔══██╗██║██║ ██║████╗ ██║██╔════╝ -// █████╗ ███████║██║██║ ██║██╔██╗ ██║██║ ███╗ -// ██╔══╝ ██╔══██║██║██║ ██║██║╚██╗██║██║ ██║ -// ██║ ██║ ██║██║███████╗██║██║ ╚████║╚██████╔╝ -// ╚═╝ ╚═╝ ╚═╝╚═╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ - -#[cfg(test)] -mod failing { - use dataurl::{DataUrl, DataUrlParseError}; - - #[test] - fn must_fall_back_to_none_if_set_to_empty() -> Result<(), DataUrlParseError> { - let mut data_url = DataUrl::new(); - - data_url.set_media_type(Some("".to_string())); - - assert_eq!(data_url.media_type_no_default(), None); - - Ok(()) - } - - #[test] - fn must_fall_back_to_none_if_attempted_to_set_to_whitespace() -> Result<(), DataUrlParseError> { - let mut data_url = DataUrl::new(); - - data_url.set_media_type(Some(" ".to_string())); - - assert_eq!(data_url.media_type_no_default(), None); - - Ok(()) - } - - // #[test] - // fn must_fall_back_to_none_if_attempted_to_set_to_bad() -> Result<(), DataUrlParseError> { - // let mut data_url = DataUrl::new(); - - // data_url.set_media_type(Some("bad".to_string())); - // assert_eq!(data_url.media_type_no_default(), None); - - // Ok(()) - // } -} diff --git a/tests/lib/mod.rs b/tests/lib/mod.rs deleted file mode 100644 index ddf32a2..0000000 --- a/tests/lib/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -// Constructors -mod new; -mod parse; - -// Getters and setters -mod charset; -mod charset_no_default; -mod fragment; -mod media_type; -mod media_type_no_default; -mod text; -mod to_string; - -// Misc. -mod _misc_; diff --git a/tests/lib/new.rs b/tests/lib/new.rs deleted file mode 100644 index 9a4952c..0000000 --- a/tests/lib/new.rs +++ /dev/null @@ -1,20 +0,0 @@ -// ██████╗ █████╗ ███████╗███████╗██╗███╗ ██╗ ██████╗ -// ██╔══██╗██╔══██╗██╔════╝██╔════╝██║████╗ ██║██╔════╝ -// ██████╔╝███████║███████╗███████╗██║██╔██╗ ██║██║ ███╗ -// ██╔═══╝ ██╔══██║╚════██║╚════██║██║██║╚██╗██║██║ ██║ -// ██║ ██║ ██║███████║███████║██║██║ ╚████║╚██████╔╝ -// ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ - -#[cfg(test)] -mod passing { - use dataurl::{DataUrl, DataUrlParseError}; - - #[test] - fn must_be_possible_to_create_new_empty_data_url() -> Result<(), DataUrlParseError> { - let data_url = DataUrl::new(); - - assert_eq!(data_url.to_string(), "data:,"); - - Ok(()) - } -} diff --git a/tests/lib/parse.rs b/tests/lib/parse.rs deleted file mode 100644 index a3c593e..0000000 --- a/tests/lib/parse.rs +++ /dev/null @@ -1,247 +0,0 @@ -// ██████╗ █████╗ ███████╗███████╗██╗███╗ ██╗ ██████╗ -// ██╔══██╗██╔══██╗██╔════╝██╔════╝██║████╗ ██║██╔════╝ -// ██████╔╝███████║███████╗███████╗██║██╔██╗ ██║██║ ███╗ -// ██╔═══╝ ██╔══██║╚════██║╚════██║██║██║╚██╗██║██║ ██║ -// ██║ ██║ ██║███████║███████║██║██║ ╚████║╚██████╔╝ -// ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ - -#[cfg(test)] -mod passing { - use dataurl::{DataUrl, DataUrlParseError}; - - #[test] - fn must_trim_spaces_around_url() -> Result<(), DataUrlParseError> { - let data_url: DataUrl = DataUrl::parse(" data:, a b ")?; - - assert_eq!(data_url.data(), " a b".as_bytes()); - - Ok(()) - } - - #[test] - fn must_be_able_to_parse_url_with_no_media_type() -> Result<(), DataUrlParseError> { - let data_url: DataUrl = DataUrl::parse("data:,Hello,%20World!")?; - - assert_eq!(data_url.text(), "Hello, World!"); - - Ok(()) - } - - #[test] - fn must_parse_query_as_part_of_data() -> Result<(), DataUrlParseError> { - let data_url: DataUrl = DataUrl::parse("data:;,Hello?World#")?; - - assert_eq!(data_url.text(), "Hello?World"); - - Ok(()) - } - - #[test] - fn must_parse_empty_query_as_part_of_data() -> Result<(), DataUrlParseError> { - let data_url: DataUrl = DataUrl::parse("data:;,Hello?#")?; - - assert_eq!(data_url.text(), "Hello?"); - - Ok(()) - } - - #[test] - fn must_parse_utf8_charset_no_media_type() -> Result<(), DataUrlParseError> { - let data_url: DataUrl = DataUrl::parse("data:;charset=utf8,")?; - - assert_eq!(data_url.charset(), "UTF-8".to_string()); - - Ok(()) - } - - #[test] - fn must_parse_utf8_charset_no_media_type_encoded() -> Result<(), DataUrlParseError> { - let data_url: DataUrl = DataUrl::parse("data:;charset=utf8;base64,")?; - - assert_eq!(data_url.charset(), "UTF-8".to_string()); - - Ok(()) - } - - #[test] - fn must_parse_utf8_charset_with_media_type() -> Result<(), DataUrlParseError> { - let data_url: DataUrl = DataUrl::parse("data:text/html;charset=utf8,")?; - - assert_eq!(data_url.charset(), "UTF-8".to_string()); - - Ok(()) - } - - #[test] - fn must_parse_utf8_charset_with_media_type_encoded() -> Result<(), DataUrlParseError> { - let data_url: DataUrl = DataUrl::parse("data:text/html;charset=utf8;base64,")?; - - assert_eq!(data_url.charset(), "UTF-8".to_string()); - - Ok(()) - } - - #[test] - fn must_parse_unicode_emoji() -> Result<(), DataUrlParseError> { - let data_url: DataUrl = DataUrl::parse("data:;charset=utf8;base64,4piA77iP")?; - - assert_eq!(data_url.data(), [226, 152, 128, 239, 184, 143]); - - Ok(()) - } - - #[test] - fn must_parse_all_caps() -> Result<(), DataUrlParseError> { - let data_url: DataUrl = DataUrl::parse("data:TEXT/CSS;CHARSET=UTF8;BASE64,w5w=")?; - - assert_eq!(data_url.media_type(), "text/css".to_string()); - assert_eq!(data_url.charset(), "UTF-8".to_string()); - assert!(data_url.is_base64_encoded()); - assert_eq!(data_url.data(), [195, 156]); - assert_eq!(data_url.text(), "Ü"); - assert_eq!(data_url.fragment(), None); - - Ok(()) - } - - #[test] - fn must_parse_unicode_emoji_if_has_extra_meta_data() -> Result<(), DataUrlParseError> { - let data_url: DataUrl = - DataUrl::parse("data:TEXT/CSS;filename=x.css;charset=Utf-8;BASE64;somethingelse,w5w=")?; - - assert_eq!(data_url.media_type(), "text/css".to_string()); - assert_eq!(data_url.charset(), "UTF-8".to_string()); - assert!(data_url.is_base64_encoded()); - assert_eq!(data_url.data(), [195, 156]); - assert_eq!(data_url.text(), "Ü"); - assert_eq!(data_url.fragment(), None); - - Ok(()) - } - - #[test] - fn must_parse_unicode_emoji_if_has_extra_meta_data_and_multiple_charsets( - ) -> Result<(), DataUrlParseError> { - let data_url: DataUrl = DataUrl::parse( - "data:TEXT/CSS;charset=Utf-8;filename=x;charset=US-ASCII;BASE64;somethingelse,w5w=", - )?; - - assert_eq!(data_url.media_type(), "text/css".to_string()); - assert_eq!(data_url.charset(), "UTF-8".to_string()); - assert!(data_url.is_base64_encoded()); - assert_eq!(data_url.data(), [195, 156]); - assert_eq!(data_url.text(), "Ü"); - assert_eq!(data_url.fragment(), None); - - Ok(()) - } - - // #[test] - // fn must_parse_unicode_emoji_if_has_extra_meta_data_and_multiple_charsets_with_spaces( - // ) -> Result<(), DataUrlParseError> { - // let data_url: DataUrl = DataUrl::parse( - // "data:TEXT/CSS; charset=Utf-8; filename=x; charset = US-ASCII; BASE64; somethingelse ,w5w=", - // )?; - - // assert_eq!(data_url.media_type(), "text/css".to_string()); - // assert_eq!(data_url.charset(), "UTF-8".to_string()); - // assert!(data_url.is_base64_encoded()); - // assert_eq!(data_url.data(), [195, 156]); - // assert_eq!(data_url.text(), "Ü"); - // assert_eq!(data_url.fragment(), None); - - // Ok(()) - // } -} - -// ███████╗ █████╗ ██╗██╗ ██╗███╗ ██╗ ██████╗ -// ██╔════╝██╔══██╗██║██║ ██║████╗ ██║██╔════╝ -// █████╗ ███████║██║██║ ██║██╔██╗ ██║██║ ███╗ -// ██╔══╝ ██╔══██║██║██║ ██║██║╚██╗██║██║ ██║ -// ██║ ██║ ██║██║███████╗██║██║ ╚████║╚██████╔╝ -// ╚═╝ ╚═╝ ╚═╝╚═╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ - -#[cfg(test)] -mod failing { - use dataurl::{DataUrl, DataUrlParseError}; - - #[test] - fn must_error_out_if_given_empty_string() -> Result<(), DataUrlParseError> { - match DataUrl::parse("") { - Ok(_data_url) => { - assert!(false); - } - Err(_data_url_parse_err) => { - assert!(true); - } - } - - Ok(()) - } - - #[test] - fn must_treat_data_as_base64_unencoded_if_no_semicolon_before_base64( - ) -> Result<(), DataUrlParseError> { - let data_url: DataUrl = DataUrl::parse("data:base64,SGVsbG8sIHdvcmxkIQo=")?; - - assert_eq!(data_url.media_type(), "text/plain".to_string()); - assert_eq!(data_url.charset(), "US-ASCII".to_string()); - assert!(!data_url.is_base64_encoded()); - assert_eq!( - String::from_utf8_lossy(data_url.data()), - "SGVsbG8sIHdvcmxkIQo=" - ); - assert_eq!(data_url.fragment(), None); - - Ok(()) - } - - #[test] - fn must_fall_back_to_us_ascii_if_given_bad_charset() -> Result<(), DataUrlParseError> { - let data_url: DataUrl = DataUrl::parse("data:;charset=BAD-CHARSET;base64,")?; - - assert_eq!(data_url.media_type(), "text/plain".to_string()); - assert_eq!(data_url.charset(), "US-ASCII".to_string()); - assert!(data_url.is_base64_encoded()); - assert_eq!(data_url.data(), []); - assert_eq!(data_url.fragment(), None); - - Ok(()) - } - - #[test] - fn must_fall_back_to_text_plain_if_given_bad_media_type() -> Result<(), DataUrlParseError> { - let data_url: DataUrl = DataUrl::parse("data:bad;,")?; - - assert_eq!(data_url.media_type(), "text/plain".to_string()); - assert_eq!(data_url.charset(), "US-ASCII".to_string()); - assert!(!data_url.is_base64_encoded()); - assert_eq!(data_url.data(), []); - assert_eq!(data_url.fragment(), None); - - Ok(()) - } - - #[test] - fn must_fall_to_parse_charset_if_put_not_right_after_media_type( - ) -> Result<(), DataUrlParseError> { - let mut data_url: DataUrl = DataUrl::parse("data:text/css;base64;charset=utf8,w5w=")?; - - assert_eq!(data_url.media_type(), "text/css".to_string()); - assert_eq!(data_url.charset(), "US-ASCII".to_string()); - assert!(data_url.is_base64_encoded()); - assert_eq!(data_url.data(), [195, 156]); - assert_eq!(data_url.fragment(), None); - assert_eq!(data_url.text(), "Ü"); - // Different from the original her because we needed to encode "Ü" as US-ASCII, which is 4 bytes - assert_eq!(data_url.to_string(), "data:text/css;base64,w5w="); - data_url.set_charset(Some("utf-8".to_string())); - // And now it should be fine - assert_eq!( - data_url.to_string(), - "data:text/css;charset=UTF-8;base64,w5w=" - ); - - Ok(()) - } -} diff --git a/tests/lib/text.rs b/tests/lib/text.rs deleted file mode 100644 index cc1efe7..0000000 --- a/tests/lib/text.rs +++ /dev/null @@ -1,74 +0,0 @@ -// ██████╗ █████╗ ███████╗███████╗██╗███╗ ██╗ ██████╗ -// ██╔══██╗██╔══██╗██╔════╝██╔════╝██║████╗ ██║██╔════╝ -// ██████╔╝███████║███████╗███████╗██║██╔██╗ ██║██║ ███╗ -// ██╔═══╝ ██╔══██║╚════██║╚════██║██║██║╚██╗██║██║ ██║ -// ██║ ██║ ██║███████║███████║██║██║ ╚████║╚██████╔╝ -// ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ - -#[cfg(test)] -mod passing { - use dataurl::{DataUrl, DataUrlParseError}; - - #[test] - fn must_be_empty_by_default() -> Result<(), DataUrlParseError> { - let data_url = DataUrl::new(); - - assert_eq!(data_url.text(), ""); - - Ok(()) - } - - #[test] - fn must_remain_empty_after_given_empty_data() -> Result<(), DataUrlParseError> { - let mut data_url = DataUrl::new(); - - data_url.set_data(&[]); - assert_eq!(data_url.text(), ""); - - Ok(()) - } - - #[test] - fn must_accept_and_return_same_ascii_text() -> Result<(), DataUrlParseError> { - let mut data_url = DataUrl::new(); - - data_url.set_data(b"some text"); - assert_eq!(data_url.text(), "some text"); - - Ok(()) - } - - #[test] - fn must_accept_and_return_same_utf8_text() -> Result<(), DataUrlParseError> { - let mut data_url = DataUrl::new(); - - data_url.set_charset(Some("utf8".to_string())); - data_url.set_data("Ü".as_bytes()); - assert_eq!(data_url.text(), "Ü"); - - Ok(()) - } -} - -// ███████╗ █████╗ ██╗██╗ ██╗███╗ ██╗ ██████╗ -// ██╔════╝██╔══██╗██║██║ ██║████╗ ██║██╔════╝ -// █████╗ ███████║██║██║ ██║██╔██╗ ██║██║ ███╗ -// ██╔══╝ ██╔══██║██║██║ ██║██║╚██╗██║██║ ██║ -// ██║ ██║ ██║██║███████╗██║██║ ╚████║╚██████╔╝ -// ╚═╝ ╚═╝ ╚═╝╚═╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ - -#[cfg(test)] -mod failing { - use dataurl::{DataUrl, DataUrlParseError}; - - #[test] - fn must_return_garbage_when_given_unicode_data_without_setting_charset_to_utf8( - ) -> Result<(), DataUrlParseError> { - let mut data_url = DataUrl::new(); - - data_url.set_data("Ü".as_bytes()); - assert_eq!(data_url.text(), "Ü"); - - Ok(()) - } -} diff --git a/tests/lib/to_string.rs b/tests/lib/to_string.rs deleted file mode 100644 index 6277a2a..0000000 --- a/tests/lib/to_string.rs +++ /dev/null @@ -1,129 +0,0 @@ -// ██████╗ █████╗ ███████╗███████╗██╗███╗ ██╗ ██████╗ -// ██╔══██╗██╔══██╗██╔════╝██╔════╝██║████╗ ██║██╔════╝ -// ██████╔╝███████║███████╗███████╗██║██╔██╗ ██║██║ ███╗ -// ██╔═══╝ ██╔══██║╚════██║╚════██║██║██║╚██╗██║██║ ██║ -// ██║ ██║ ██║███████║███████║██║██║ ╚████║╚██████╔╝ -// ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ - -#[cfg(test)] -mod passing { - use dataurl::{DataUrl, DataUrlParseError}; - - #[test] - fn must_be_empty_by_default() -> Result<(), DataUrlParseError> { - let data_url = DataUrl::new(); - - assert_eq!(data_url.to_string(), "data:,"); - - Ok(()) - } - - #[test] - fn must_be_empty_with_base64_if_set_encoded_to_true() -> Result<(), DataUrlParseError> { - let mut data_url = DataUrl::new(); - - data_url.set_is_base64_encoded(true); - - assert_eq!(data_url.to_string(), "data:;base64,"); - - Ok(()) - } - - #[test] - fn must_be_empty_with_proper_media_type_and_base64_if_encoded_but_no_data( - ) -> Result<(), DataUrlParseError> { - let mut data_url = DataUrl::new(); - - data_url.set_media_type(Some("text/html".to_string())); - data_url.set_is_base64_encoded(true); - - assert_eq!(data_url.to_string(), "data:text/html;base64,"); - - Ok(()) - } - - #[test] - fn must_not_reflect_default_charset_even_if_it_was_set() -> Result<(), DataUrlParseError> { - let mut data_url = DataUrl::new(); - - data_url.set_media_type(Some("text/html".to_string())); - data_url.set_charset(Some("US-ASCII".to_string())); - data_url.set_is_base64_encoded(true); - - assert_eq!(data_url.to_string(), "data:text/html;base64,"); - - Ok(()) - } - - #[test] - fn must_reflect_non_default_charset_if_provided() -> Result<(), DataUrlParseError> { - let mut data_url = DataUrl::new(); - - data_url.set_media_type(Some("text/html".to_string())); - data_url.set_charset(Some("utf8".to_string())); - data_url.set_is_base64_encoded(true); - - assert_eq!(data_url.to_string(), "data:text/html;charset=UTF-8;base64,"); - - Ok(()) - } - - #[test] - fn must_properly_encode_utf8_emoji_in_base64() -> Result<(), DataUrlParseError> { - let mut data_url = DataUrl::new(); - - data_url.set_media_type(Some("text/html".to_string())); - data_url.set_charset(Some("utf8".to_string())); - data_url.set_is_base64_encoded(true); - data_url.set_data(&[0xe2, 0x98, 0x80, 0xef, 0xb8, 0x8f]); // Sun emoji in UTF-8 bytes - - assert_eq!( - data_url.to_string(), - "data:text/html;charset=UTF-8;base64,4piA77iP" - ); - - Ok(()) - } - - #[test] - fn must_properly_encode_utf8_emoji() -> Result<(), DataUrlParseError> { - let mut data_url = DataUrl::new(); - - data_url.set_media_type(Some("text/html".to_string())); - data_url.set_charset(Some("utf8".to_string())); - data_url.set_data(&[0xe2, 0x98, 0x80, 0xef, 0xb8, 0x8f]); // Sun emoji in UTF-8 bytes - - assert_eq!( - data_url.to_string(), - "data:text/html;charset=UTF-8,%E2%98%80%EF%B8%8F" - ); - - Ok(()) - } -} - -// ███████╗ █████╗ ██╗██╗ ██╗███╗ ██╗ ██████╗ -// ██╔════╝██╔══██╗██║██║ ██║████╗ ██║██╔════╝ -// █████╗ ███████║██║██║ ██║██╔██╗ ██║██║ ███╗ -// ██╔══╝ ██╔══██║██║██║ ██║██║╚██╗██║██║ ██║ -// ██║ ██║ ██║██║███████╗██║██║ ╚████║╚██████╔╝ -// ╚═╝ ╚═╝ ╚═╝╚═╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ - -#[cfg(test)] -mod failing { - use dataurl::{DataUrl, DataUrlParseError}; - - #[test] - fn must_not_output_default_charset_if_bad_charset_given() -> Result<(), DataUrlParseError> { - let mut data_url = DataUrl::new(); - - data_url.set_media_type(Some("text/html".to_string())); - data_url.set_charset(Some("utf8".to_string())); // This must set charset to UTF-8 - data_url.set_charset(Some("BAD-CHARSET".to_string())); // And this bad input must make it fall back to US-ASCII - data_url.set_is_base64_encoded(true); - - assert_eq!(data_url.to_string(), "data:text/html;base64,"); - - Ok(()) - } -} diff --git a/tests/mod.rs b/tests/mod.rs deleted file mode 100644 index 3e6b1f7..0000000 --- a/tests/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod cli; -mod lib; diff --git a/tests/wpt.rs b/tests/wpt.rs new file mode 100644 index 0000000..d43b077 --- /dev/null +++ b/tests/wpt.rs @@ -0,0 +1,56 @@ +//! Web Platform Tests suites for data URLs (https://github.com/web-platform-tests/wpt). +//! See tests/wpt/README.md for where the fixtures come from. + +mod common; + +use common::json; +use dataurl::DataUrl; + +#[test] +fn wpt_data_urls() { + let fixture = json::parse(include_str!("wpt/data-urls.json")); + assert!(!fixture.as_array().is_empty()); + + for test in fixture.as_array() { + let test = test.as_array(); + let input = test[0].as_str(); + + match test[1].as_opt_str() { + Some(expected_mime_type) => { + let data_url = DataUrl::parse(input) + .unwrap_or_else(|e| panic!("{input:?} must be parsed, got {e:?}")); + assert_eq!(data_url.mime_type(), expected_mime_type, "{input:?}"); + assert_eq!(data_url.data(), test[2].as_bytes(), "{input:?}"); + + // Serializing and parsing again must yield the same result + let reparsed = DataUrl::parse(&data_url.to_string()).unwrap(); + assert_eq!(reparsed.mime_type(), expected_mime_type, "{input:?}"); + assert_eq!(reparsed.data(), data_url.data(), "{input:?}"); + } + None => assert!(DataUrl::parse(input).is_err(), "{input:?} must fail"), + } + } +} + +#[test] +fn wpt_forgiving_base64() { + let fixture = json::parse(include_str!("wpt/base64.json")); + assert!(!fixture.as_array().is_empty()); + + for test in fixture.as_array() { + let test = test.as_array(); + let input = format!("data:;base64,{}", test[0].as_str()); + + if test[1].is_null() { + assert!(DataUrl::parse(&input).is_err(), "{input:?} must fail"); + } else { + let data_url = DataUrl::parse(&input) + .unwrap_or_else(|e| panic!("{input:?} must be parsed, got {e:?}")); + assert_eq!(data_url.data(), test[1].as_bytes(), "{input:?}"); + assert_eq!( + DataUrl::parse(&data_url.to_string()).unwrap().data(), + data_url.data() + ); + } + } +} diff --git a/tests/wpt/README.md b/tests/wpt/README.md new file mode 100644 index 0000000..574a43f --- /dev/null +++ b/tests/wpt/README.md @@ -0,0 +1,25 @@ +# Web Platform Tests fixtures + +These files are unmodified copies from https://github.com/web-platform-tests/wpt, +taken at commit `3bcf9ec39dc0f83e20f5665a988c12c0dff5f1e9` (2026-10-16): + +| File | Source | +|-----------------------------|----------------------------------------------------------| +| `data-urls.json` | `fetch/data-urls/resources/data-urls.json` | +| `base64.json` | `fetch/data-urls/resources/base64.json` | +| `mime-types.json` | `mimesniff/mime-types/resources/mime-types.json` | +| `generated-mime-types.json` | `mimesniff/mime-types/resources/generated-mime-types.json` | + +To update them, copy the newer versions over and run `cargo test`. + +They are distributed under the 3-Clause BSD License: + +> Copyright © web-platform-tests contributors +> +> Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +> +> 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +> 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +> 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +> +> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/tests/wpt/base64.json b/tests/wpt/base64.json new file mode 100644 index 0000000..01f981a --- /dev/null +++ b/tests/wpt/base64.json @@ -0,0 +1,82 @@ +[ + ["", []], + ["abcd", [105, 183, 29]], + [" abcd", [105, 183, 29]], + ["abcd ", [105, 183, 29]], + [" abcd===", null], + ["abcd=== ", null], + ["abcd ===", null], + ["a", null], + ["ab", [105]], + ["abc", [105, 183]], + ["abcde", null], + ["𐀀", null], + ["=", null], + ["==", null], + ["===", null], + ["====", null], + ["=====", null], + ["a=", null], + ["a==", null], + ["a===", null], + ["a====", null], + ["a=====", null], + ["ab=", null], + ["ab==", [105]], + ["ab===", null], + ["ab====", null], + ["ab=====", null], + ["abc=", [105, 183]], + ["abc==", null], + ["abc===", null], + ["abc====", null], + ["abc=====", null], + ["abcd=", null], + ["abcd==", null], + ["abcd===", null], + ["abcd====", null], + ["abcd=====", null], + ["abcde=", null], + ["abcde==", null], + ["abcde===", null], + ["abcde====", null], + ["abcde=====", null], + ["=a", null], + ["=a=", null], + ["a=b", null], + ["a=b=", null], + ["ab=c", null], + ["ab=c=", null], + ["abc=d", null], + ["abc=d=", null], + ["ab\u000Bcd", null], + ["ab\u3000cd", null], + ["ab\u3001cd", null], + ["ab\tcd", [105, 183, 29]], + ["ab\ncd", [105, 183, 29]], + ["ab\fcd", [105, 183, 29]], + ["ab\rcd", [105, 183, 29]], + ["ab cd", [105, 183, 29]], + ["ab\u00a0cd", null], + ["ab\t\n\f\r cd", [105, 183, 29]], + [" \t\n\f\r ab\t\n\f\r cd\t\n\f\r ", [105, 183, 29]], + ["ab\t\n\f\r =\t\n\f\r =\t\n\f\r ", [105]], + ["A", null], + ["/A", [252]], + ["//A", [255, 240]], + ["///A", [255, 255, 192]], + ["////A", null], + ["/", null], + ["A/", [3]], + ["AA/", [0, 15]], + ["AAAA/", null], + ["AAA/", [0, 0, 63]], + ["\u0000nonsense", null], + ["abcd\u0000nonsense", null], + ["YQ", [97]], + ["YR", [97]], + ["~~", null], + ["..", null], + ["--", null], + ["__", null] +] diff --git a/tests/wpt/data-urls.json b/tests/wpt/data-urls.json new file mode 100644 index 0000000..f318d1f --- /dev/null +++ b/tests/wpt/data-urls.json @@ -0,0 +1,214 @@ +[ + ["data://test/,X", + "text/plain;charset=US-ASCII", + [88]], + ["data://test:test/,X", + null], + ["data:,X", + "text/plain;charset=US-ASCII", + [88]], + ["data:", + null], + ["data:text/html", + null], + ["data:text/html ;charset=x ", + null], + ["data:,", + "text/plain;charset=US-ASCII", + []], + ["data:,X#X", + "text/plain;charset=US-ASCII", + [88]], + ["data:,%FF", + "text/plain;charset=US-ASCII", + [255]], + ["data:text/plain,X", + "text/plain", + [88]], + ["data:text/plain ,X", + "text/plain", + [88]], + ["data:text/plain%20,X", + "text/plain%20", + [88]], + ["data:text/plain\f,X", + "text/plain%0c", + [88]], + ["data:text/plain%0C,X", + "text/plain%0c", + [88]], + ["data:text/plain;,X", + "text/plain", + [88]], + ["data:;x=x;charset=x,X", + "text/plain;x=x;charset=x", + [88]], + ["data:;x=x,X", + "text/plain;x=x", + [88]], + ["data:text/plain;charset=windows-1252,%C2%B1", + "text/plain;charset=windows-1252", + [194, 177]], + ["data:text/plain;Charset=UTF-8,%C2%B1", + "text/plain;charset=UTF-8", + [194, 177]], + ["data:text/plain;charset=windows-1252,áñçə💩", + "text/plain;charset=windows-1252", + [195, 161, 195, 177, 195, 167, 201, 153, 240, 159, 146, 169]], + ["data:text/plain;charset=UTF-8,áñçə💩", + "text/plain;charset=UTF-8", + [195, 161, 195, 177, 195, 167, 201, 153, 240, 159, 146, 169]], + ["data:image/gif,%C2%B1", + "image/gif", + [194, 177]], + ["data:IMAGE/gif,%C2%B1", + "image/gif", + [194, 177]], + ["data:IMAGE/gif;hi=x,%C2%B1", + "image/gif;hi=x", + [194, 177]], + ["data:IMAGE/gif;CHARSET=x,%C2%B1", + "image/gif;charset=x", + [194, 177]], + ["data: ,%FF", + "text/plain;charset=US-ASCII", + [255]], + ["data:%20,%FF", + "text/plain;charset=US-ASCII", + [255]], + ["data:\f,%FF", + "text/plain;charset=US-ASCII", + [255]], + ["data:%1F,%FF", + "text/plain;charset=US-ASCII", + [255]], + ["data:\u0000,%FF", + "text/plain;charset=US-ASCII", + [255]], + ["data:%00,%FF", + "text/plain;charset=US-ASCII", + [255]], + ["data:text/html ,X", + "text/html", + [88]], + ["data:text / html,X", + "text/plain;charset=US-ASCII", + [88]], + ["data:†,X", + "text/plain;charset=US-ASCII", + [88]], + ["data:†/†,X", + "%e2%80%a0/%e2%80%a0", + [88]], + ["data:X,X", + "text/plain;charset=US-ASCII", + [88]], + ["data:image/png,X X", + "image/png", + [88, 32, 88]], + ["data:application/javascript,X X", + "application/javascript", + [88, 32, 88]], + ["data:application/xml,X X", + "application/xml", + [88, 32, 88]], + ["data:text/javascript,X X", + "text/javascript", + [88, 32, 88]], + ["data:text/plain,X X", + "text/plain", + [88, 32, 88]], + ["data:unknown/unknown,X X", + "unknown/unknown", + [88, 32, 88]], + ["data:text/plain;a=\",\",X", + "text/plain;a=\"\"", + [34, 44, 88]], + ["data:text/plain;a=%2C,X", + "text/plain;a=%2C", + [88]], + ["data:;base64;base64,WA", + "text/plain", + [88]], + ["data:x/x;base64;base64,WA", + "x/x", + [88]], + ["data:x/x;base64;charset=x,WA", + "x/x;charset=x", + [87, 65]], + ["data:x/x;base64;charset=x;base64,WA", + "x/x;charset=x", + [88]], + ["data:x/x;base64;base64x,WA", + "x/x", + [87, 65]], + ["data:;base64,W%20A", + "text/plain;charset=US-ASCII", + [88]], + ["data:;base64,W%0CA", + "text/plain;charset=US-ASCII", + [88]], + ["data:x;base64x,WA", + "text/plain;charset=US-ASCII", + [87, 65]], + ["data:x;base64;x,WA", + "text/plain;charset=US-ASCII", + [87, 65]], + ["data:x;base64=x,WA", + "text/plain;charset=US-ASCII", + [87, 65]], + ["data:; base64,WA", + "text/plain;charset=US-ASCII", + [88]], + ["data:; base64,WA", + "text/plain;charset=US-ASCII", + [88]], + ["data: ;charset=x ; base64,WA", + "text/plain;charset=x", + [88]], + ["data:;base64;,WA", + "text/plain", + [87, 65]], + ["data:;base64 ,WA", + "text/plain;charset=US-ASCII", + [88]], + ["data:;base64 ,WA", + "text/plain;charset=US-ASCII", + [88]], + ["data:;base 64,WA", + "text/plain", + [87, 65]], + ["data:;BASe64,WA", + "text/plain;charset=US-ASCII", + [88]], + ["data:;%62ase64,WA", + "text/plain", + [87, 65]], + ["data:%3Bbase64,WA", + "text/plain;charset=US-ASCII", + [87, 65]], + ["data:;charset=x,X", + "text/plain;charset=x", + [88]], + ["data:; charset=x,X", + "text/plain;charset=x", + [88]], + ["data:;charset =x,X", + "text/plain", + [88]], + ["data:;charset= x,X", + "text/plain;charset=\" x\"", + [88]], + ["data:;charset=,X", + "text/plain", + [88]], + ["data:;charset,X", + "text/plain", + [88]], + ["data:;charset=\"x\",X", + "text/plain;charset=x", + [88]], + ["data:;CHARSET=\"X\",X", + "text/plain;charset=X", + [88]] +] diff --git a/tests/wpt/generated-mime-types.json b/tests/wpt/generated-mime-types.json new file mode 100644 index 0000000..f8934da --- /dev/null +++ b/tests/wpt/generated-mime-types.json @@ -0,0 +1,3526 @@ +[ + { + "input": "\u0000/x", + "output": null + }, + { + "input": "x/\u0000", + "output": null + }, + { + "input": "x/x;\u0000=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0000;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\"\u0000\";bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\u0001/x", + "output": null + }, + { + "input": "x/\u0001", + "output": null + }, + { + "input": "x/x;\u0001=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0001;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\"\u0001\";bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\u0002/x", + "output": null + }, + { + "input": "x/\u0002", + "output": null + }, + { + "input": "x/x;\u0002=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0002;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\"\u0002\";bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\u0003/x", + "output": null + }, + { + "input": "x/\u0003", + "output": null + }, + { + "input": "x/x;\u0003=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0003;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\"\u0003\";bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\u0004/x", + "output": null + }, + { + "input": "x/\u0004", + "output": null + }, + { + "input": "x/x;\u0004=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0004;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\"\u0004\";bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\u0005/x", + "output": null + }, + { + "input": "x/\u0005", + "output": null + }, + { + "input": "x/x;\u0005=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0005;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\"\u0005\";bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\u0006/x", + "output": null + }, + { + "input": "x/\u0006", + "output": null + }, + { + "input": "x/x;\u0006=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0006;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\"\u0006\";bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\u0007/x", + "output": null + }, + { + "input": "x/\u0007", + "output": null + }, + { + "input": "x/x;\u0007=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0007;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\"\u0007\";bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\b/x", + "output": null + }, + { + "input": "x/\b", + "output": null + }, + { + "input": "x/x;\b=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\b;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\"\b\";bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\t/x", + "output": null + }, + { + "input": "x/\t", + "output": null + }, + { + "input": "x/x;\t=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\n/x", + "output": null + }, + { + "input": "x/\n", + "output": null + }, + { + "input": "x/x;\n=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\n;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\"\n\";bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\u000b/x", + "output": null + }, + { + "input": "x/\u000b", + "output": null + }, + { + "input": "x/x;\u000b=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u000b;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\"\u000b\";bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\f/x", + "output": null + }, + { + "input": "x/\f", + "output": null + }, + { + "input": "x/x;\f=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\f;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\"\f\";bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\r/x", + "output": null + }, + { + "input": "x/\r", + "output": null + }, + { + "input": "x/x;\r=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\r;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\"\r\";bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\u000e/x", + "output": null + }, + { + "input": "x/\u000e", + "output": null + }, + { + "input": "x/x;\u000e=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u000e;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\"\u000e\";bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\u000f/x", + "output": null + }, + { + "input": "x/\u000f", + "output": null + }, + { + "input": "x/x;\u000f=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u000f;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\"\u000f\";bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\u0010/x", + "output": null + }, + { + "input": "x/\u0010", + "output": null + }, + { + "input": "x/x;\u0010=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0010;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\"\u0010\";bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\u0011/x", + "output": null + }, + { + "input": "x/\u0011", + "output": null + }, + { + "input": "x/x;\u0011=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0011;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\"\u0011\";bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\u0012/x", + "output": null + }, + { + "input": "x/\u0012", + "output": null + }, + { + "input": "x/x;\u0012=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0012;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\"\u0012\";bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\u0013/x", + "output": null + }, + { + "input": "x/\u0013", + "output": null + }, + { + "input": "x/x;\u0013=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0013;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\"\u0013\";bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\u0014/x", + "output": null + }, + { + "input": "x/\u0014", + "output": null + }, + { + "input": "x/x;\u0014=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0014;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\"\u0014\";bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\u0015/x", + "output": null + }, + { + "input": "x/\u0015", + "output": null + }, + { + "input": "x/x;\u0015=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0015;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\"\u0015\";bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\u0016/x", + "output": null + }, + { + "input": "x/\u0016", + "output": null + }, + { + "input": "x/x;\u0016=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0016;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\"\u0016\";bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\u0017/x", + "output": null + }, + { + "input": "x/\u0017", + "output": null + }, + { + "input": "x/x;\u0017=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0017;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\"\u0017\";bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\u0018/x", + "output": null + }, + { + "input": "x/\u0018", + "output": null + }, + { + "input": "x/x;\u0018=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0018;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\"\u0018\";bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\u0019/x", + "output": null + }, + { + "input": "x/\u0019", + "output": null + }, + { + "input": "x/x;\u0019=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0019;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\"\u0019\";bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\u001a/x", + "output": null + }, + { + "input": "x/\u001a", + "output": null + }, + { + "input": "x/x;\u001a=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u001a;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\"\u001a\";bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\u001b/x", + "output": null + }, + { + "input": "x/\u001b", + "output": null + }, + { + "input": "x/x;\u001b=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u001b;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\"\u001b\";bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\u001c/x", + "output": null + }, + { + "input": "x/\u001c", + "output": null + }, + { + "input": "x/x;\u001c=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u001c;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\"\u001c\";bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\u001d/x", + "output": null + }, + { + "input": "x/\u001d", + "output": null + }, + { + "input": "x/x;\u001d=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u001d;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\"\u001d\";bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\u001e/x", + "output": null + }, + { + "input": "x/\u001e", + "output": null + }, + { + "input": "x/x;\u001e=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u001e;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\"\u001e\";bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\u001f/x", + "output": null + }, + { + "input": "x/\u001f", + "output": null + }, + { + "input": "x/x;\u001f=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u001f;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\"\u001f\";bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": " /x", + "output": null + }, + { + "input": "x/ ", + "output": null + }, + { + "input": "x/x; =x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\"/x", + "output": null + }, + { + "input": "x/\"", + "output": null + }, + { + "input": "x/x;\"=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "(/x", + "output": null + }, + { + "input": "x/(", + "output": null + }, + { + "input": "x/x;(=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=(;bonus=x", + "output": "x/x;x=\"(\";bonus=x" + }, + { + "input": "x/x;x=\"(\";bonus=x", + "output": "x/x;x=\"(\";bonus=x" + }, + { + "input": ")/x", + "output": null + }, + { + "input": "x/)", + "output": null + }, + { + "input": "x/x;)=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=);bonus=x", + "output": "x/x;x=\")\";bonus=x" + }, + { + "input": "x/x;x=\")\";bonus=x", + "output": "x/x;x=\")\";bonus=x" + }, + { + "input": ",/x", + "output": null + }, + { + "input": "x/,", + "output": null + }, + { + "input": "x/x;,=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=,;bonus=x", + "output": "x/x;x=\",\";bonus=x" + }, + { + "input": "x/x;x=\",\";bonus=x", + "output": "x/x;x=\",\";bonus=x" + }, + { + "input": "x/x;/=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=/;bonus=x", + "output": "x/x;x=\"/\";bonus=x" + }, + { + "input": "x/x;x=\"/\";bonus=x", + "output": "x/x;x=\"/\";bonus=x" + }, + { + "input": ":/x", + "output": null + }, + { + "input": "x/:", + "output": null + }, + { + "input": "x/x;:=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=:;bonus=x", + "output": "x/x;x=\":\";bonus=x" + }, + { + "input": "x/x;x=\":\";bonus=x", + "output": "x/x;x=\":\";bonus=x" + }, + { + "input": ";/x", + "output": null + }, + { + "input": "x/;", + "output": null + }, + { + "input": "/x", + "output": null + }, + { + "input": "x/>", + "output": null + }, + { + "input": "x/x;>=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=>;bonus=x", + "output": "x/x;x=\">\";bonus=x" + }, + { + "input": "x/x;x=\">\";bonus=x", + "output": "x/x;x=\">\";bonus=x" + }, + { + "input": "?/x", + "output": null + }, + { + "input": "x/?", + "output": null + }, + { + "input": "x/x;?=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=?;bonus=x", + "output": "x/x;x=\"?\";bonus=x" + }, + { + "input": "x/x;x=\"?\";bonus=x", + "output": "x/x;x=\"?\";bonus=x" + }, + { + "input": "@/x", + "output": null + }, + { + "input": "x/@", + "output": null + }, + { + "input": "x/x;@=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=@;bonus=x", + "output": "x/x;x=\"@\";bonus=x" + }, + { + "input": "x/x;x=\"@\";bonus=x", + "output": "x/x;x=\"@\";bonus=x" + }, + { + "input": "[/x", + "output": null + }, + { + "input": "x/[", + "output": null + }, + { + "input": "x/x;[=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=[;bonus=x", + "output": "x/x;x=\"[\";bonus=x" + }, + { + "input": "x/x;x=\"[\";bonus=x", + "output": "x/x;x=\"[\";bonus=x" + }, + { + "input": "\\/x", + "output": null + }, + { + "input": "x/\\", + "output": null + }, + { + "input": "x/x;\\=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "]/x", + "output": null + }, + { + "input": "x/]", + "output": null + }, + { + "input": "x/x;]=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=];bonus=x", + "output": "x/x;x=\"]\";bonus=x" + }, + { + "input": "x/x;x=\"]\";bonus=x", + "output": "x/x;x=\"]\";bonus=x" + }, + { + "input": "{/x", + "output": null + }, + { + "input": "x/{", + "output": null + }, + { + "input": "x/x;{=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x={;bonus=x", + "output": "x/x;x=\"{\";bonus=x" + }, + { + "input": "x/x;x=\"{\";bonus=x", + "output": "x/x;x=\"{\";bonus=x" + }, + { + "input": "}/x", + "output": null + }, + { + "input": "x/}", + "output": null + }, + { + "input": "x/x;}=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=};bonus=x", + "output": "x/x;x=\"}\";bonus=x" + }, + { + "input": "x/x;x=\"}\";bonus=x", + "output": "x/x;x=\"}\";bonus=x" + }, + { + "input": "\u007f/x", + "output": null + }, + { + "input": "x/\u007f", + "output": null + }, + { + "input": "x/x;\u007f=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u007f;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\"\u007f\";bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "\u0080/x", + "output": null + }, + { + "input": "x/\u0080", + "output": null + }, + { + "input": "x/x;\u0080=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0080;bonus=x", + "output": "x/x;x=\"\u0080\";bonus=x" + }, + { + "input": "x/x;x=\"\u0080\";bonus=x", + "output": "x/x;x=\"\u0080\";bonus=x" + }, + { + "input": "\u0081/x", + "output": null + }, + { + "input": "x/\u0081", + "output": null + }, + { + "input": "x/x;\u0081=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0081;bonus=x", + "output": "x/x;x=\"\u0081\";bonus=x" + }, + { + "input": "x/x;x=\"\u0081\";bonus=x", + "output": "x/x;x=\"\u0081\";bonus=x" + }, + { + "input": "\u0082/x", + "output": null + }, + { + "input": "x/\u0082", + "output": null + }, + { + "input": "x/x;\u0082=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0082;bonus=x", + "output": "x/x;x=\"\u0082\";bonus=x" + }, + { + "input": "x/x;x=\"\u0082\";bonus=x", + "output": "x/x;x=\"\u0082\";bonus=x" + }, + { + "input": "\u0083/x", + "output": null + }, + { + "input": "x/\u0083", + "output": null + }, + { + "input": "x/x;\u0083=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0083;bonus=x", + "output": "x/x;x=\"\u0083\";bonus=x" + }, + { + "input": "x/x;x=\"\u0083\";bonus=x", + "output": "x/x;x=\"\u0083\";bonus=x" + }, + { + "input": "\u0084/x", + "output": null + }, + { + "input": "x/\u0084", + "output": null + }, + { + "input": "x/x;\u0084=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0084;bonus=x", + "output": "x/x;x=\"\u0084\";bonus=x" + }, + { + "input": "x/x;x=\"\u0084\";bonus=x", + "output": "x/x;x=\"\u0084\";bonus=x" + }, + { + "input": "\u0085/x", + "output": null + }, + { + "input": "x/\u0085", + "output": null + }, + { + "input": "x/x;\u0085=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0085;bonus=x", + "output": "x/x;x=\"\u0085\";bonus=x" + }, + { + "input": "x/x;x=\"\u0085\";bonus=x", + "output": "x/x;x=\"\u0085\";bonus=x" + }, + { + "input": "\u0086/x", + "output": null + }, + { + "input": "x/\u0086", + "output": null + }, + { + "input": "x/x;\u0086=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0086;bonus=x", + "output": "x/x;x=\"\u0086\";bonus=x" + }, + { + "input": "x/x;x=\"\u0086\";bonus=x", + "output": "x/x;x=\"\u0086\";bonus=x" + }, + { + "input": "\u0087/x", + "output": null + }, + { + "input": "x/\u0087", + "output": null + }, + { + "input": "x/x;\u0087=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0087;bonus=x", + "output": "x/x;x=\"\u0087\";bonus=x" + }, + { + "input": "x/x;x=\"\u0087\";bonus=x", + "output": "x/x;x=\"\u0087\";bonus=x" + }, + { + "input": "\u0088/x", + "output": null + }, + { + "input": "x/\u0088", + "output": null + }, + { + "input": "x/x;\u0088=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0088;bonus=x", + "output": "x/x;x=\"\u0088\";bonus=x" + }, + { + "input": "x/x;x=\"\u0088\";bonus=x", + "output": "x/x;x=\"\u0088\";bonus=x" + }, + { + "input": "\u0089/x", + "output": null + }, + { + "input": "x/\u0089", + "output": null + }, + { + "input": "x/x;\u0089=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0089;bonus=x", + "output": "x/x;x=\"\u0089\";bonus=x" + }, + { + "input": "x/x;x=\"\u0089\";bonus=x", + "output": "x/x;x=\"\u0089\";bonus=x" + }, + { + "input": "\u008a/x", + "output": null + }, + { + "input": "x/\u008a", + "output": null + }, + { + "input": "x/x;\u008a=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u008a;bonus=x", + "output": "x/x;x=\"\u008a\";bonus=x" + }, + { + "input": "x/x;x=\"\u008a\";bonus=x", + "output": "x/x;x=\"\u008a\";bonus=x" + }, + { + "input": "\u008b/x", + "output": null + }, + { + "input": "x/\u008b", + "output": null + }, + { + "input": "x/x;\u008b=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u008b;bonus=x", + "output": "x/x;x=\"\u008b\";bonus=x" + }, + { + "input": "x/x;x=\"\u008b\";bonus=x", + "output": "x/x;x=\"\u008b\";bonus=x" + }, + { + "input": "\u008c/x", + "output": null + }, + { + "input": "x/\u008c", + "output": null + }, + { + "input": "x/x;\u008c=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u008c;bonus=x", + "output": "x/x;x=\"\u008c\";bonus=x" + }, + { + "input": "x/x;x=\"\u008c\";bonus=x", + "output": "x/x;x=\"\u008c\";bonus=x" + }, + { + "input": "\u008d/x", + "output": null + }, + { + "input": "x/\u008d", + "output": null + }, + { + "input": "x/x;\u008d=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u008d;bonus=x", + "output": "x/x;x=\"\u008d\";bonus=x" + }, + { + "input": "x/x;x=\"\u008d\";bonus=x", + "output": "x/x;x=\"\u008d\";bonus=x" + }, + { + "input": "\u008e/x", + "output": null + }, + { + "input": "x/\u008e", + "output": null + }, + { + "input": "x/x;\u008e=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u008e;bonus=x", + "output": "x/x;x=\"\u008e\";bonus=x" + }, + { + "input": "x/x;x=\"\u008e\";bonus=x", + "output": "x/x;x=\"\u008e\";bonus=x" + }, + { + "input": "\u008f/x", + "output": null + }, + { + "input": "x/\u008f", + "output": null + }, + { + "input": "x/x;\u008f=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u008f;bonus=x", + "output": "x/x;x=\"\u008f\";bonus=x" + }, + { + "input": "x/x;x=\"\u008f\";bonus=x", + "output": "x/x;x=\"\u008f\";bonus=x" + }, + { + "input": "\u0090/x", + "output": null + }, + { + "input": "x/\u0090", + "output": null + }, + { + "input": "x/x;\u0090=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0090;bonus=x", + "output": "x/x;x=\"\u0090\";bonus=x" + }, + { + "input": "x/x;x=\"\u0090\";bonus=x", + "output": "x/x;x=\"\u0090\";bonus=x" + }, + { + "input": "\u0091/x", + "output": null + }, + { + "input": "x/\u0091", + "output": null + }, + { + "input": "x/x;\u0091=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0091;bonus=x", + "output": "x/x;x=\"\u0091\";bonus=x" + }, + { + "input": "x/x;x=\"\u0091\";bonus=x", + "output": "x/x;x=\"\u0091\";bonus=x" + }, + { + "input": "\u0092/x", + "output": null + }, + { + "input": "x/\u0092", + "output": null + }, + { + "input": "x/x;\u0092=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0092;bonus=x", + "output": "x/x;x=\"\u0092\";bonus=x" + }, + { + "input": "x/x;x=\"\u0092\";bonus=x", + "output": "x/x;x=\"\u0092\";bonus=x" + }, + { + "input": "\u0093/x", + "output": null + }, + { + "input": "x/\u0093", + "output": null + }, + { + "input": "x/x;\u0093=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0093;bonus=x", + "output": "x/x;x=\"\u0093\";bonus=x" + }, + { + "input": "x/x;x=\"\u0093\";bonus=x", + "output": "x/x;x=\"\u0093\";bonus=x" + }, + { + "input": "\u0094/x", + "output": null + }, + { + "input": "x/\u0094", + "output": null + }, + { + "input": "x/x;\u0094=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0094;bonus=x", + "output": "x/x;x=\"\u0094\";bonus=x" + }, + { + "input": "x/x;x=\"\u0094\";bonus=x", + "output": "x/x;x=\"\u0094\";bonus=x" + }, + { + "input": "\u0095/x", + "output": null + }, + { + "input": "x/\u0095", + "output": null + }, + { + "input": "x/x;\u0095=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0095;bonus=x", + "output": "x/x;x=\"\u0095\";bonus=x" + }, + { + "input": "x/x;x=\"\u0095\";bonus=x", + "output": "x/x;x=\"\u0095\";bonus=x" + }, + { + "input": "\u0096/x", + "output": null + }, + { + "input": "x/\u0096", + "output": null + }, + { + "input": "x/x;\u0096=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0096;bonus=x", + "output": "x/x;x=\"\u0096\";bonus=x" + }, + { + "input": "x/x;x=\"\u0096\";bonus=x", + "output": "x/x;x=\"\u0096\";bonus=x" + }, + { + "input": "\u0097/x", + "output": null + }, + { + "input": "x/\u0097", + "output": null + }, + { + "input": "x/x;\u0097=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0097;bonus=x", + "output": "x/x;x=\"\u0097\";bonus=x" + }, + { + "input": "x/x;x=\"\u0097\";bonus=x", + "output": "x/x;x=\"\u0097\";bonus=x" + }, + { + "input": "\u0098/x", + "output": null + }, + { + "input": "x/\u0098", + "output": null + }, + { + "input": "x/x;\u0098=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0098;bonus=x", + "output": "x/x;x=\"\u0098\";bonus=x" + }, + { + "input": "x/x;x=\"\u0098\";bonus=x", + "output": "x/x;x=\"\u0098\";bonus=x" + }, + { + "input": "\u0099/x", + "output": null + }, + { + "input": "x/\u0099", + "output": null + }, + { + "input": "x/x;\u0099=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u0099;bonus=x", + "output": "x/x;x=\"\u0099\";bonus=x" + }, + { + "input": "x/x;x=\"\u0099\";bonus=x", + "output": "x/x;x=\"\u0099\";bonus=x" + }, + { + "input": "\u009a/x", + "output": null + }, + { + "input": "x/\u009a", + "output": null + }, + { + "input": "x/x;\u009a=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u009a;bonus=x", + "output": "x/x;x=\"\u009a\";bonus=x" + }, + { + "input": "x/x;x=\"\u009a\";bonus=x", + "output": "x/x;x=\"\u009a\";bonus=x" + }, + { + "input": "\u009b/x", + "output": null + }, + { + "input": "x/\u009b", + "output": null + }, + { + "input": "x/x;\u009b=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u009b;bonus=x", + "output": "x/x;x=\"\u009b\";bonus=x" + }, + { + "input": "x/x;x=\"\u009b\";bonus=x", + "output": "x/x;x=\"\u009b\";bonus=x" + }, + { + "input": "\u009c/x", + "output": null + }, + { + "input": "x/\u009c", + "output": null + }, + { + "input": "x/x;\u009c=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u009c;bonus=x", + "output": "x/x;x=\"\u009c\";bonus=x" + }, + { + "input": "x/x;x=\"\u009c\";bonus=x", + "output": "x/x;x=\"\u009c\";bonus=x" + }, + { + "input": "\u009d/x", + "output": null + }, + { + "input": "x/\u009d", + "output": null + }, + { + "input": "x/x;\u009d=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u009d;bonus=x", + "output": "x/x;x=\"\u009d\";bonus=x" + }, + { + "input": "x/x;x=\"\u009d\";bonus=x", + "output": "x/x;x=\"\u009d\";bonus=x" + }, + { + "input": "\u009e/x", + "output": null + }, + { + "input": "x/\u009e", + "output": null + }, + { + "input": "x/x;\u009e=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u009e;bonus=x", + "output": "x/x;x=\"\u009e\";bonus=x" + }, + { + "input": "x/x;x=\"\u009e\";bonus=x", + "output": "x/x;x=\"\u009e\";bonus=x" + }, + { + "input": "\u009f/x", + "output": null + }, + { + "input": "x/\u009f", + "output": null + }, + { + "input": "x/x;\u009f=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u009f;bonus=x", + "output": "x/x;x=\"\u009f\";bonus=x" + }, + { + "input": "x/x;x=\"\u009f\";bonus=x", + "output": "x/x;x=\"\u009f\";bonus=x" + }, + { + "input": "\u00a0/x", + "output": null + }, + { + "input": "x/\u00a0", + "output": null + }, + { + "input": "x/x;\u00a0=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00a0;bonus=x", + "output": "x/x;x=\"\u00a0\";bonus=x" + }, + { + "input": "x/x;x=\"\u00a0\";bonus=x", + "output": "x/x;x=\"\u00a0\";bonus=x" + }, + { + "input": "\u00a1/x", + "output": null + }, + { + "input": "x/\u00a1", + "output": null + }, + { + "input": "x/x;\u00a1=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00a1;bonus=x", + "output": "x/x;x=\"\u00a1\";bonus=x" + }, + { + "input": "x/x;x=\"\u00a1\";bonus=x", + "output": "x/x;x=\"\u00a1\";bonus=x" + }, + { + "input": "\u00a2/x", + "output": null + }, + { + "input": "x/\u00a2", + "output": null + }, + { + "input": "x/x;\u00a2=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00a2;bonus=x", + "output": "x/x;x=\"\u00a2\";bonus=x" + }, + { + "input": "x/x;x=\"\u00a2\";bonus=x", + "output": "x/x;x=\"\u00a2\";bonus=x" + }, + { + "input": "\u00a3/x", + "output": null + }, + { + "input": "x/\u00a3", + "output": null + }, + { + "input": "x/x;\u00a3=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00a3;bonus=x", + "output": "x/x;x=\"\u00a3\";bonus=x" + }, + { + "input": "x/x;x=\"\u00a3\";bonus=x", + "output": "x/x;x=\"\u00a3\";bonus=x" + }, + { + "input": "\u00a4/x", + "output": null + }, + { + "input": "x/\u00a4", + "output": null + }, + { + "input": "x/x;\u00a4=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00a4;bonus=x", + "output": "x/x;x=\"\u00a4\";bonus=x" + }, + { + "input": "x/x;x=\"\u00a4\";bonus=x", + "output": "x/x;x=\"\u00a4\";bonus=x" + }, + { + "input": "\u00a5/x", + "output": null + }, + { + "input": "x/\u00a5", + "output": null + }, + { + "input": "x/x;\u00a5=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00a5;bonus=x", + "output": "x/x;x=\"\u00a5\";bonus=x" + }, + { + "input": "x/x;x=\"\u00a5\";bonus=x", + "output": "x/x;x=\"\u00a5\";bonus=x" + }, + { + "input": "\u00a6/x", + "output": null + }, + { + "input": "x/\u00a6", + "output": null + }, + { + "input": "x/x;\u00a6=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00a6;bonus=x", + "output": "x/x;x=\"\u00a6\";bonus=x" + }, + { + "input": "x/x;x=\"\u00a6\";bonus=x", + "output": "x/x;x=\"\u00a6\";bonus=x" + }, + { + "input": "\u00a7/x", + "output": null + }, + { + "input": "x/\u00a7", + "output": null + }, + { + "input": "x/x;\u00a7=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00a7;bonus=x", + "output": "x/x;x=\"\u00a7\";bonus=x" + }, + { + "input": "x/x;x=\"\u00a7\";bonus=x", + "output": "x/x;x=\"\u00a7\";bonus=x" + }, + { + "input": "\u00a8/x", + "output": null + }, + { + "input": "x/\u00a8", + "output": null + }, + { + "input": "x/x;\u00a8=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00a8;bonus=x", + "output": "x/x;x=\"\u00a8\";bonus=x" + }, + { + "input": "x/x;x=\"\u00a8\";bonus=x", + "output": "x/x;x=\"\u00a8\";bonus=x" + }, + { + "input": "\u00a9/x", + "output": null + }, + { + "input": "x/\u00a9", + "output": null + }, + { + "input": "x/x;\u00a9=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00a9;bonus=x", + "output": "x/x;x=\"\u00a9\";bonus=x" + }, + { + "input": "x/x;x=\"\u00a9\";bonus=x", + "output": "x/x;x=\"\u00a9\";bonus=x" + }, + { + "input": "\u00aa/x", + "output": null + }, + { + "input": "x/\u00aa", + "output": null + }, + { + "input": "x/x;\u00aa=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00aa;bonus=x", + "output": "x/x;x=\"\u00aa\";bonus=x" + }, + { + "input": "x/x;x=\"\u00aa\";bonus=x", + "output": "x/x;x=\"\u00aa\";bonus=x" + }, + { + "input": "\u00ab/x", + "output": null + }, + { + "input": "x/\u00ab", + "output": null + }, + { + "input": "x/x;\u00ab=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00ab;bonus=x", + "output": "x/x;x=\"\u00ab\";bonus=x" + }, + { + "input": "x/x;x=\"\u00ab\";bonus=x", + "output": "x/x;x=\"\u00ab\";bonus=x" + }, + { + "input": "\u00ac/x", + "output": null + }, + { + "input": "x/\u00ac", + "output": null + }, + { + "input": "x/x;\u00ac=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00ac;bonus=x", + "output": "x/x;x=\"\u00ac\";bonus=x" + }, + { + "input": "x/x;x=\"\u00ac\";bonus=x", + "output": "x/x;x=\"\u00ac\";bonus=x" + }, + { + "input": "\u00ad/x", + "output": null + }, + { + "input": "x/\u00ad", + "output": null + }, + { + "input": "x/x;\u00ad=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00ad;bonus=x", + "output": "x/x;x=\"\u00ad\";bonus=x" + }, + { + "input": "x/x;x=\"\u00ad\";bonus=x", + "output": "x/x;x=\"\u00ad\";bonus=x" + }, + { + "input": "\u00ae/x", + "output": null + }, + { + "input": "x/\u00ae", + "output": null + }, + { + "input": "x/x;\u00ae=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00ae;bonus=x", + "output": "x/x;x=\"\u00ae\";bonus=x" + }, + { + "input": "x/x;x=\"\u00ae\";bonus=x", + "output": "x/x;x=\"\u00ae\";bonus=x" + }, + { + "input": "\u00af/x", + "output": null + }, + { + "input": "x/\u00af", + "output": null + }, + { + "input": "x/x;\u00af=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00af;bonus=x", + "output": "x/x;x=\"\u00af\";bonus=x" + }, + { + "input": "x/x;x=\"\u00af\";bonus=x", + "output": "x/x;x=\"\u00af\";bonus=x" + }, + { + "input": "\u00b0/x", + "output": null + }, + { + "input": "x/\u00b0", + "output": null + }, + { + "input": "x/x;\u00b0=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00b0;bonus=x", + "output": "x/x;x=\"\u00b0\";bonus=x" + }, + { + "input": "x/x;x=\"\u00b0\";bonus=x", + "output": "x/x;x=\"\u00b0\";bonus=x" + }, + { + "input": "\u00b1/x", + "output": null + }, + { + "input": "x/\u00b1", + "output": null + }, + { + "input": "x/x;\u00b1=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00b1;bonus=x", + "output": "x/x;x=\"\u00b1\";bonus=x" + }, + { + "input": "x/x;x=\"\u00b1\";bonus=x", + "output": "x/x;x=\"\u00b1\";bonus=x" + }, + { + "input": "\u00b2/x", + "output": null + }, + { + "input": "x/\u00b2", + "output": null + }, + { + "input": "x/x;\u00b2=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00b2;bonus=x", + "output": "x/x;x=\"\u00b2\";bonus=x" + }, + { + "input": "x/x;x=\"\u00b2\";bonus=x", + "output": "x/x;x=\"\u00b2\";bonus=x" + }, + { + "input": "\u00b3/x", + "output": null + }, + { + "input": "x/\u00b3", + "output": null + }, + { + "input": "x/x;\u00b3=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00b3;bonus=x", + "output": "x/x;x=\"\u00b3\";bonus=x" + }, + { + "input": "x/x;x=\"\u00b3\";bonus=x", + "output": "x/x;x=\"\u00b3\";bonus=x" + }, + { + "input": "\u00b4/x", + "output": null + }, + { + "input": "x/\u00b4", + "output": null + }, + { + "input": "x/x;\u00b4=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00b4;bonus=x", + "output": "x/x;x=\"\u00b4\";bonus=x" + }, + { + "input": "x/x;x=\"\u00b4\";bonus=x", + "output": "x/x;x=\"\u00b4\";bonus=x" + }, + { + "input": "\u00b5/x", + "output": null + }, + { + "input": "x/\u00b5", + "output": null + }, + { + "input": "x/x;\u00b5=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00b5;bonus=x", + "output": "x/x;x=\"\u00b5\";bonus=x" + }, + { + "input": "x/x;x=\"\u00b5\";bonus=x", + "output": "x/x;x=\"\u00b5\";bonus=x" + }, + { + "input": "\u00b6/x", + "output": null + }, + { + "input": "x/\u00b6", + "output": null + }, + { + "input": "x/x;\u00b6=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00b6;bonus=x", + "output": "x/x;x=\"\u00b6\";bonus=x" + }, + { + "input": "x/x;x=\"\u00b6\";bonus=x", + "output": "x/x;x=\"\u00b6\";bonus=x" + }, + { + "input": "\u00b7/x", + "output": null + }, + { + "input": "x/\u00b7", + "output": null + }, + { + "input": "x/x;\u00b7=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00b7;bonus=x", + "output": "x/x;x=\"\u00b7\";bonus=x" + }, + { + "input": "x/x;x=\"\u00b7\";bonus=x", + "output": "x/x;x=\"\u00b7\";bonus=x" + }, + { + "input": "\u00b8/x", + "output": null + }, + { + "input": "x/\u00b8", + "output": null + }, + { + "input": "x/x;\u00b8=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00b8;bonus=x", + "output": "x/x;x=\"\u00b8\";bonus=x" + }, + { + "input": "x/x;x=\"\u00b8\";bonus=x", + "output": "x/x;x=\"\u00b8\";bonus=x" + }, + { + "input": "\u00b9/x", + "output": null + }, + { + "input": "x/\u00b9", + "output": null + }, + { + "input": "x/x;\u00b9=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00b9;bonus=x", + "output": "x/x;x=\"\u00b9\";bonus=x" + }, + { + "input": "x/x;x=\"\u00b9\";bonus=x", + "output": "x/x;x=\"\u00b9\";bonus=x" + }, + { + "input": "\u00ba/x", + "output": null + }, + { + "input": "x/\u00ba", + "output": null + }, + { + "input": "x/x;\u00ba=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00ba;bonus=x", + "output": "x/x;x=\"\u00ba\";bonus=x" + }, + { + "input": "x/x;x=\"\u00ba\";bonus=x", + "output": "x/x;x=\"\u00ba\";bonus=x" + }, + { + "input": "\u00bb/x", + "output": null + }, + { + "input": "x/\u00bb", + "output": null + }, + { + "input": "x/x;\u00bb=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00bb;bonus=x", + "output": "x/x;x=\"\u00bb\";bonus=x" + }, + { + "input": "x/x;x=\"\u00bb\";bonus=x", + "output": "x/x;x=\"\u00bb\";bonus=x" + }, + { + "input": "\u00bc/x", + "output": null + }, + { + "input": "x/\u00bc", + "output": null + }, + { + "input": "x/x;\u00bc=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00bc;bonus=x", + "output": "x/x;x=\"\u00bc\";bonus=x" + }, + { + "input": "x/x;x=\"\u00bc\";bonus=x", + "output": "x/x;x=\"\u00bc\";bonus=x" + }, + { + "input": "\u00bd/x", + "output": null + }, + { + "input": "x/\u00bd", + "output": null + }, + { + "input": "x/x;\u00bd=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00bd;bonus=x", + "output": "x/x;x=\"\u00bd\";bonus=x" + }, + { + "input": "x/x;x=\"\u00bd\";bonus=x", + "output": "x/x;x=\"\u00bd\";bonus=x" + }, + { + "input": "\u00be/x", + "output": null + }, + { + "input": "x/\u00be", + "output": null + }, + { + "input": "x/x;\u00be=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00be;bonus=x", + "output": "x/x;x=\"\u00be\";bonus=x" + }, + { + "input": "x/x;x=\"\u00be\";bonus=x", + "output": "x/x;x=\"\u00be\";bonus=x" + }, + { + "input": "\u00bf/x", + "output": null + }, + { + "input": "x/\u00bf", + "output": null + }, + { + "input": "x/x;\u00bf=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00bf;bonus=x", + "output": "x/x;x=\"\u00bf\";bonus=x" + }, + { + "input": "x/x;x=\"\u00bf\";bonus=x", + "output": "x/x;x=\"\u00bf\";bonus=x" + }, + { + "input": "\u00c0/x", + "output": null + }, + { + "input": "x/\u00c0", + "output": null + }, + { + "input": "x/x;\u00c0=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00c0;bonus=x", + "output": "x/x;x=\"\u00c0\";bonus=x" + }, + { + "input": "x/x;x=\"\u00c0\";bonus=x", + "output": "x/x;x=\"\u00c0\";bonus=x" + }, + { + "input": "\u00c1/x", + "output": null + }, + { + "input": "x/\u00c1", + "output": null + }, + { + "input": "x/x;\u00c1=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00c1;bonus=x", + "output": "x/x;x=\"\u00c1\";bonus=x" + }, + { + "input": "x/x;x=\"\u00c1\";bonus=x", + "output": "x/x;x=\"\u00c1\";bonus=x" + }, + { + "input": "\u00c2/x", + "output": null + }, + { + "input": "x/\u00c2", + "output": null + }, + { + "input": "x/x;\u00c2=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00c2;bonus=x", + "output": "x/x;x=\"\u00c2\";bonus=x" + }, + { + "input": "x/x;x=\"\u00c2\";bonus=x", + "output": "x/x;x=\"\u00c2\";bonus=x" + }, + { + "input": "\u00c3/x", + "output": null + }, + { + "input": "x/\u00c3", + "output": null + }, + { + "input": "x/x;\u00c3=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00c3;bonus=x", + "output": "x/x;x=\"\u00c3\";bonus=x" + }, + { + "input": "x/x;x=\"\u00c3\";bonus=x", + "output": "x/x;x=\"\u00c3\";bonus=x" + }, + { + "input": "\u00c4/x", + "output": null + }, + { + "input": "x/\u00c4", + "output": null + }, + { + "input": "x/x;\u00c4=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00c4;bonus=x", + "output": "x/x;x=\"\u00c4\";bonus=x" + }, + { + "input": "x/x;x=\"\u00c4\";bonus=x", + "output": "x/x;x=\"\u00c4\";bonus=x" + }, + { + "input": "\u00c5/x", + "output": null + }, + { + "input": "x/\u00c5", + "output": null + }, + { + "input": "x/x;\u00c5=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00c5;bonus=x", + "output": "x/x;x=\"\u00c5\";bonus=x" + }, + { + "input": "x/x;x=\"\u00c5\";bonus=x", + "output": "x/x;x=\"\u00c5\";bonus=x" + }, + { + "input": "\u00c6/x", + "output": null + }, + { + "input": "x/\u00c6", + "output": null + }, + { + "input": "x/x;\u00c6=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00c6;bonus=x", + "output": "x/x;x=\"\u00c6\";bonus=x" + }, + { + "input": "x/x;x=\"\u00c6\";bonus=x", + "output": "x/x;x=\"\u00c6\";bonus=x" + }, + { + "input": "\u00c7/x", + "output": null + }, + { + "input": "x/\u00c7", + "output": null + }, + { + "input": "x/x;\u00c7=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00c7;bonus=x", + "output": "x/x;x=\"\u00c7\";bonus=x" + }, + { + "input": "x/x;x=\"\u00c7\";bonus=x", + "output": "x/x;x=\"\u00c7\";bonus=x" + }, + { + "input": "\u00c8/x", + "output": null + }, + { + "input": "x/\u00c8", + "output": null + }, + { + "input": "x/x;\u00c8=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00c8;bonus=x", + "output": "x/x;x=\"\u00c8\";bonus=x" + }, + { + "input": "x/x;x=\"\u00c8\";bonus=x", + "output": "x/x;x=\"\u00c8\";bonus=x" + }, + { + "input": "\u00c9/x", + "output": null + }, + { + "input": "x/\u00c9", + "output": null + }, + { + "input": "x/x;\u00c9=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00c9;bonus=x", + "output": "x/x;x=\"\u00c9\";bonus=x" + }, + { + "input": "x/x;x=\"\u00c9\";bonus=x", + "output": "x/x;x=\"\u00c9\";bonus=x" + }, + { + "input": "\u00ca/x", + "output": null + }, + { + "input": "x/\u00ca", + "output": null + }, + { + "input": "x/x;\u00ca=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00ca;bonus=x", + "output": "x/x;x=\"\u00ca\";bonus=x" + }, + { + "input": "x/x;x=\"\u00ca\";bonus=x", + "output": "x/x;x=\"\u00ca\";bonus=x" + }, + { + "input": "\u00cb/x", + "output": null + }, + { + "input": "x/\u00cb", + "output": null + }, + { + "input": "x/x;\u00cb=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00cb;bonus=x", + "output": "x/x;x=\"\u00cb\";bonus=x" + }, + { + "input": "x/x;x=\"\u00cb\";bonus=x", + "output": "x/x;x=\"\u00cb\";bonus=x" + }, + { + "input": "\u00cc/x", + "output": null + }, + { + "input": "x/\u00cc", + "output": null + }, + { + "input": "x/x;\u00cc=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00cc;bonus=x", + "output": "x/x;x=\"\u00cc\";bonus=x" + }, + { + "input": "x/x;x=\"\u00cc\";bonus=x", + "output": "x/x;x=\"\u00cc\";bonus=x" + }, + { + "input": "\u00cd/x", + "output": null + }, + { + "input": "x/\u00cd", + "output": null + }, + { + "input": "x/x;\u00cd=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00cd;bonus=x", + "output": "x/x;x=\"\u00cd\";bonus=x" + }, + { + "input": "x/x;x=\"\u00cd\";bonus=x", + "output": "x/x;x=\"\u00cd\";bonus=x" + }, + { + "input": "\u00ce/x", + "output": null + }, + { + "input": "x/\u00ce", + "output": null + }, + { + "input": "x/x;\u00ce=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00ce;bonus=x", + "output": "x/x;x=\"\u00ce\";bonus=x" + }, + { + "input": "x/x;x=\"\u00ce\";bonus=x", + "output": "x/x;x=\"\u00ce\";bonus=x" + }, + { + "input": "\u00cf/x", + "output": null + }, + { + "input": "x/\u00cf", + "output": null + }, + { + "input": "x/x;\u00cf=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00cf;bonus=x", + "output": "x/x;x=\"\u00cf\";bonus=x" + }, + { + "input": "x/x;x=\"\u00cf\";bonus=x", + "output": "x/x;x=\"\u00cf\";bonus=x" + }, + { + "input": "\u00d0/x", + "output": null + }, + { + "input": "x/\u00d0", + "output": null + }, + { + "input": "x/x;\u00d0=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00d0;bonus=x", + "output": "x/x;x=\"\u00d0\";bonus=x" + }, + { + "input": "x/x;x=\"\u00d0\";bonus=x", + "output": "x/x;x=\"\u00d0\";bonus=x" + }, + { + "input": "\u00d1/x", + "output": null + }, + { + "input": "x/\u00d1", + "output": null + }, + { + "input": "x/x;\u00d1=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00d1;bonus=x", + "output": "x/x;x=\"\u00d1\";bonus=x" + }, + { + "input": "x/x;x=\"\u00d1\";bonus=x", + "output": "x/x;x=\"\u00d1\";bonus=x" + }, + { + "input": "\u00d2/x", + "output": null + }, + { + "input": "x/\u00d2", + "output": null + }, + { + "input": "x/x;\u00d2=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00d2;bonus=x", + "output": "x/x;x=\"\u00d2\";bonus=x" + }, + { + "input": "x/x;x=\"\u00d2\";bonus=x", + "output": "x/x;x=\"\u00d2\";bonus=x" + }, + { + "input": "\u00d3/x", + "output": null + }, + { + "input": "x/\u00d3", + "output": null + }, + { + "input": "x/x;\u00d3=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00d3;bonus=x", + "output": "x/x;x=\"\u00d3\";bonus=x" + }, + { + "input": "x/x;x=\"\u00d3\";bonus=x", + "output": "x/x;x=\"\u00d3\";bonus=x" + }, + { + "input": "\u00d4/x", + "output": null + }, + { + "input": "x/\u00d4", + "output": null + }, + { + "input": "x/x;\u00d4=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00d4;bonus=x", + "output": "x/x;x=\"\u00d4\";bonus=x" + }, + { + "input": "x/x;x=\"\u00d4\";bonus=x", + "output": "x/x;x=\"\u00d4\";bonus=x" + }, + { + "input": "\u00d5/x", + "output": null + }, + { + "input": "x/\u00d5", + "output": null + }, + { + "input": "x/x;\u00d5=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00d5;bonus=x", + "output": "x/x;x=\"\u00d5\";bonus=x" + }, + { + "input": "x/x;x=\"\u00d5\";bonus=x", + "output": "x/x;x=\"\u00d5\";bonus=x" + }, + { + "input": "\u00d6/x", + "output": null + }, + { + "input": "x/\u00d6", + "output": null + }, + { + "input": "x/x;\u00d6=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00d6;bonus=x", + "output": "x/x;x=\"\u00d6\";bonus=x" + }, + { + "input": "x/x;x=\"\u00d6\";bonus=x", + "output": "x/x;x=\"\u00d6\";bonus=x" + }, + { + "input": "\u00d7/x", + "output": null + }, + { + "input": "x/\u00d7", + "output": null + }, + { + "input": "x/x;\u00d7=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00d7;bonus=x", + "output": "x/x;x=\"\u00d7\";bonus=x" + }, + { + "input": "x/x;x=\"\u00d7\";bonus=x", + "output": "x/x;x=\"\u00d7\";bonus=x" + }, + { + "input": "\u00d8/x", + "output": null + }, + { + "input": "x/\u00d8", + "output": null + }, + { + "input": "x/x;\u00d8=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00d8;bonus=x", + "output": "x/x;x=\"\u00d8\";bonus=x" + }, + { + "input": "x/x;x=\"\u00d8\";bonus=x", + "output": "x/x;x=\"\u00d8\";bonus=x" + }, + { + "input": "\u00d9/x", + "output": null + }, + { + "input": "x/\u00d9", + "output": null + }, + { + "input": "x/x;\u00d9=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00d9;bonus=x", + "output": "x/x;x=\"\u00d9\";bonus=x" + }, + { + "input": "x/x;x=\"\u00d9\";bonus=x", + "output": "x/x;x=\"\u00d9\";bonus=x" + }, + { + "input": "\u00da/x", + "output": null + }, + { + "input": "x/\u00da", + "output": null + }, + { + "input": "x/x;\u00da=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00da;bonus=x", + "output": "x/x;x=\"\u00da\";bonus=x" + }, + { + "input": "x/x;x=\"\u00da\";bonus=x", + "output": "x/x;x=\"\u00da\";bonus=x" + }, + { + "input": "\u00db/x", + "output": null + }, + { + "input": "x/\u00db", + "output": null + }, + { + "input": "x/x;\u00db=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00db;bonus=x", + "output": "x/x;x=\"\u00db\";bonus=x" + }, + { + "input": "x/x;x=\"\u00db\";bonus=x", + "output": "x/x;x=\"\u00db\";bonus=x" + }, + { + "input": "\u00dc/x", + "output": null + }, + { + "input": "x/\u00dc", + "output": null + }, + { + "input": "x/x;\u00dc=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00dc;bonus=x", + "output": "x/x;x=\"\u00dc\";bonus=x" + }, + { + "input": "x/x;x=\"\u00dc\";bonus=x", + "output": "x/x;x=\"\u00dc\";bonus=x" + }, + { + "input": "\u00dd/x", + "output": null + }, + { + "input": "x/\u00dd", + "output": null + }, + { + "input": "x/x;\u00dd=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00dd;bonus=x", + "output": "x/x;x=\"\u00dd\";bonus=x" + }, + { + "input": "x/x;x=\"\u00dd\";bonus=x", + "output": "x/x;x=\"\u00dd\";bonus=x" + }, + { + "input": "\u00de/x", + "output": null + }, + { + "input": "x/\u00de", + "output": null + }, + { + "input": "x/x;\u00de=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00de;bonus=x", + "output": "x/x;x=\"\u00de\";bonus=x" + }, + { + "input": "x/x;x=\"\u00de\";bonus=x", + "output": "x/x;x=\"\u00de\";bonus=x" + }, + { + "input": "\u00df/x", + "output": null + }, + { + "input": "x/\u00df", + "output": null + }, + { + "input": "x/x;\u00df=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00df;bonus=x", + "output": "x/x;x=\"\u00df\";bonus=x" + }, + { + "input": "x/x;x=\"\u00df\";bonus=x", + "output": "x/x;x=\"\u00df\";bonus=x" + }, + { + "input": "\u00e0/x", + "output": null + }, + { + "input": "x/\u00e0", + "output": null + }, + { + "input": "x/x;\u00e0=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00e0;bonus=x", + "output": "x/x;x=\"\u00e0\";bonus=x" + }, + { + "input": "x/x;x=\"\u00e0\";bonus=x", + "output": "x/x;x=\"\u00e0\";bonus=x" + }, + { + "input": "\u00e1/x", + "output": null + }, + { + "input": "x/\u00e1", + "output": null + }, + { + "input": "x/x;\u00e1=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00e1;bonus=x", + "output": "x/x;x=\"\u00e1\";bonus=x" + }, + { + "input": "x/x;x=\"\u00e1\";bonus=x", + "output": "x/x;x=\"\u00e1\";bonus=x" + }, + { + "input": "\u00e2/x", + "output": null + }, + { + "input": "x/\u00e2", + "output": null + }, + { + "input": "x/x;\u00e2=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00e2;bonus=x", + "output": "x/x;x=\"\u00e2\";bonus=x" + }, + { + "input": "x/x;x=\"\u00e2\";bonus=x", + "output": "x/x;x=\"\u00e2\";bonus=x" + }, + { + "input": "\u00e3/x", + "output": null + }, + { + "input": "x/\u00e3", + "output": null + }, + { + "input": "x/x;\u00e3=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00e3;bonus=x", + "output": "x/x;x=\"\u00e3\";bonus=x" + }, + { + "input": "x/x;x=\"\u00e3\";bonus=x", + "output": "x/x;x=\"\u00e3\";bonus=x" + }, + { + "input": "\u00e4/x", + "output": null + }, + { + "input": "x/\u00e4", + "output": null + }, + { + "input": "x/x;\u00e4=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00e4;bonus=x", + "output": "x/x;x=\"\u00e4\";bonus=x" + }, + { + "input": "x/x;x=\"\u00e4\";bonus=x", + "output": "x/x;x=\"\u00e4\";bonus=x" + }, + { + "input": "\u00e5/x", + "output": null + }, + { + "input": "x/\u00e5", + "output": null + }, + { + "input": "x/x;\u00e5=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00e5;bonus=x", + "output": "x/x;x=\"\u00e5\";bonus=x" + }, + { + "input": "x/x;x=\"\u00e5\";bonus=x", + "output": "x/x;x=\"\u00e5\";bonus=x" + }, + { + "input": "\u00e6/x", + "output": null + }, + { + "input": "x/\u00e6", + "output": null + }, + { + "input": "x/x;\u00e6=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00e6;bonus=x", + "output": "x/x;x=\"\u00e6\";bonus=x" + }, + { + "input": "x/x;x=\"\u00e6\";bonus=x", + "output": "x/x;x=\"\u00e6\";bonus=x" + }, + { + "input": "\u00e7/x", + "output": null + }, + { + "input": "x/\u00e7", + "output": null + }, + { + "input": "x/x;\u00e7=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00e7;bonus=x", + "output": "x/x;x=\"\u00e7\";bonus=x" + }, + { + "input": "x/x;x=\"\u00e7\";bonus=x", + "output": "x/x;x=\"\u00e7\";bonus=x" + }, + { + "input": "\u00e8/x", + "output": null + }, + { + "input": "x/\u00e8", + "output": null + }, + { + "input": "x/x;\u00e8=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00e8;bonus=x", + "output": "x/x;x=\"\u00e8\";bonus=x" + }, + { + "input": "x/x;x=\"\u00e8\";bonus=x", + "output": "x/x;x=\"\u00e8\";bonus=x" + }, + { + "input": "\u00e9/x", + "output": null + }, + { + "input": "x/\u00e9", + "output": null + }, + { + "input": "x/x;\u00e9=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00e9;bonus=x", + "output": "x/x;x=\"\u00e9\";bonus=x" + }, + { + "input": "x/x;x=\"\u00e9\";bonus=x", + "output": "x/x;x=\"\u00e9\";bonus=x" + }, + { + "input": "\u00ea/x", + "output": null + }, + { + "input": "x/\u00ea", + "output": null + }, + { + "input": "x/x;\u00ea=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00ea;bonus=x", + "output": "x/x;x=\"\u00ea\";bonus=x" + }, + { + "input": "x/x;x=\"\u00ea\";bonus=x", + "output": "x/x;x=\"\u00ea\";bonus=x" + }, + { + "input": "\u00eb/x", + "output": null + }, + { + "input": "x/\u00eb", + "output": null + }, + { + "input": "x/x;\u00eb=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00eb;bonus=x", + "output": "x/x;x=\"\u00eb\";bonus=x" + }, + { + "input": "x/x;x=\"\u00eb\";bonus=x", + "output": "x/x;x=\"\u00eb\";bonus=x" + }, + { + "input": "\u00ec/x", + "output": null + }, + { + "input": "x/\u00ec", + "output": null + }, + { + "input": "x/x;\u00ec=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00ec;bonus=x", + "output": "x/x;x=\"\u00ec\";bonus=x" + }, + { + "input": "x/x;x=\"\u00ec\";bonus=x", + "output": "x/x;x=\"\u00ec\";bonus=x" + }, + { + "input": "\u00ed/x", + "output": null + }, + { + "input": "x/\u00ed", + "output": null + }, + { + "input": "x/x;\u00ed=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00ed;bonus=x", + "output": "x/x;x=\"\u00ed\";bonus=x" + }, + { + "input": "x/x;x=\"\u00ed\";bonus=x", + "output": "x/x;x=\"\u00ed\";bonus=x" + }, + { + "input": "\u00ee/x", + "output": null + }, + { + "input": "x/\u00ee", + "output": null + }, + { + "input": "x/x;\u00ee=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00ee;bonus=x", + "output": "x/x;x=\"\u00ee\";bonus=x" + }, + { + "input": "x/x;x=\"\u00ee\";bonus=x", + "output": "x/x;x=\"\u00ee\";bonus=x" + }, + { + "input": "\u00ef/x", + "output": null + }, + { + "input": "x/\u00ef", + "output": null + }, + { + "input": "x/x;\u00ef=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00ef;bonus=x", + "output": "x/x;x=\"\u00ef\";bonus=x" + }, + { + "input": "x/x;x=\"\u00ef\";bonus=x", + "output": "x/x;x=\"\u00ef\";bonus=x" + }, + { + "input": "\u00f0/x", + "output": null + }, + { + "input": "x/\u00f0", + "output": null + }, + { + "input": "x/x;\u00f0=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00f0;bonus=x", + "output": "x/x;x=\"\u00f0\";bonus=x" + }, + { + "input": "x/x;x=\"\u00f0\";bonus=x", + "output": "x/x;x=\"\u00f0\";bonus=x" + }, + { + "input": "\u00f1/x", + "output": null + }, + { + "input": "x/\u00f1", + "output": null + }, + { + "input": "x/x;\u00f1=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00f1;bonus=x", + "output": "x/x;x=\"\u00f1\";bonus=x" + }, + { + "input": "x/x;x=\"\u00f1\";bonus=x", + "output": "x/x;x=\"\u00f1\";bonus=x" + }, + { + "input": "\u00f2/x", + "output": null + }, + { + "input": "x/\u00f2", + "output": null + }, + { + "input": "x/x;\u00f2=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00f2;bonus=x", + "output": "x/x;x=\"\u00f2\";bonus=x" + }, + { + "input": "x/x;x=\"\u00f2\";bonus=x", + "output": "x/x;x=\"\u00f2\";bonus=x" + }, + { + "input": "\u00f3/x", + "output": null + }, + { + "input": "x/\u00f3", + "output": null + }, + { + "input": "x/x;\u00f3=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00f3;bonus=x", + "output": "x/x;x=\"\u00f3\";bonus=x" + }, + { + "input": "x/x;x=\"\u00f3\";bonus=x", + "output": "x/x;x=\"\u00f3\";bonus=x" + }, + { + "input": "\u00f4/x", + "output": null + }, + { + "input": "x/\u00f4", + "output": null + }, + { + "input": "x/x;\u00f4=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00f4;bonus=x", + "output": "x/x;x=\"\u00f4\";bonus=x" + }, + { + "input": "x/x;x=\"\u00f4\";bonus=x", + "output": "x/x;x=\"\u00f4\";bonus=x" + }, + { + "input": "\u00f5/x", + "output": null + }, + { + "input": "x/\u00f5", + "output": null + }, + { + "input": "x/x;\u00f5=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00f5;bonus=x", + "output": "x/x;x=\"\u00f5\";bonus=x" + }, + { + "input": "x/x;x=\"\u00f5\";bonus=x", + "output": "x/x;x=\"\u00f5\";bonus=x" + }, + { + "input": "\u00f6/x", + "output": null + }, + { + "input": "x/\u00f6", + "output": null + }, + { + "input": "x/x;\u00f6=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00f6;bonus=x", + "output": "x/x;x=\"\u00f6\";bonus=x" + }, + { + "input": "x/x;x=\"\u00f6\";bonus=x", + "output": "x/x;x=\"\u00f6\";bonus=x" + }, + { + "input": "\u00f7/x", + "output": null + }, + { + "input": "x/\u00f7", + "output": null + }, + { + "input": "x/x;\u00f7=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00f7;bonus=x", + "output": "x/x;x=\"\u00f7\";bonus=x" + }, + { + "input": "x/x;x=\"\u00f7\";bonus=x", + "output": "x/x;x=\"\u00f7\";bonus=x" + }, + { + "input": "\u00f8/x", + "output": null + }, + { + "input": "x/\u00f8", + "output": null + }, + { + "input": "x/x;\u00f8=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00f8;bonus=x", + "output": "x/x;x=\"\u00f8\";bonus=x" + }, + { + "input": "x/x;x=\"\u00f8\";bonus=x", + "output": "x/x;x=\"\u00f8\";bonus=x" + }, + { + "input": "\u00f9/x", + "output": null + }, + { + "input": "x/\u00f9", + "output": null + }, + { + "input": "x/x;\u00f9=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00f9;bonus=x", + "output": "x/x;x=\"\u00f9\";bonus=x" + }, + { + "input": "x/x;x=\"\u00f9\";bonus=x", + "output": "x/x;x=\"\u00f9\";bonus=x" + }, + { + "input": "\u00fa/x", + "output": null + }, + { + "input": "x/\u00fa", + "output": null + }, + { + "input": "x/x;\u00fa=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00fa;bonus=x", + "output": "x/x;x=\"\u00fa\";bonus=x" + }, + { + "input": "x/x;x=\"\u00fa\";bonus=x", + "output": "x/x;x=\"\u00fa\";bonus=x" + }, + { + "input": "\u00fb/x", + "output": null + }, + { + "input": "x/\u00fb", + "output": null + }, + { + "input": "x/x;\u00fb=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00fb;bonus=x", + "output": "x/x;x=\"\u00fb\";bonus=x" + }, + { + "input": "x/x;x=\"\u00fb\";bonus=x", + "output": "x/x;x=\"\u00fb\";bonus=x" + }, + { + "input": "\u00fc/x", + "output": null + }, + { + "input": "x/\u00fc", + "output": null + }, + { + "input": "x/x;\u00fc=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00fc;bonus=x", + "output": "x/x;x=\"\u00fc\";bonus=x" + }, + { + "input": "x/x;x=\"\u00fc\";bonus=x", + "output": "x/x;x=\"\u00fc\";bonus=x" + }, + { + "input": "\u00fd/x", + "output": null + }, + { + "input": "x/\u00fd", + "output": null + }, + { + "input": "x/x;\u00fd=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00fd;bonus=x", + "output": "x/x;x=\"\u00fd\";bonus=x" + }, + { + "input": "x/x;x=\"\u00fd\";bonus=x", + "output": "x/x;x=\"\u00fd\";bonus=x" + }, + { + "input": "\u00fe/x", + "output": null + }, + { + "input": "x/\u00fe", + "output": null + }, + { + "input": "x/x;\u00fe=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00fe;bonus=x", + "output": "x/x;x=\"\u00fe\";bonus=x" + }, + { + "input": "x/x;x=\"\u00fe\";bonus=x", + "output": "x/x;x=\"\u00fe\";bonus=x" + }, + { + "input": "\u00ff/x", + "output": null + }, + { + "input": "x/\u00ff", + "output": null + }, + { + "input": "x/x;\u00ff=x;bonus=x", + "output": "x/x;bonus=x" + }, + { + "input": "x/x;x=\u00ff;bonus=x", + "output": "x/x;x=\"\u00ff\";bonus=x" + }, + { + "input": "x/x;x=\"\u00ff\";bonus=x", + "output": "x/x;x=\"\u00ff\";bonus=x" + } +] diff --git a/tests/wpt/mime-types.json b/tests/wpt/mime-types.json new file mode 100644 index 0000000..8692d77 --- /dev/null +++ b/tests/wpt/mime-types.json @@ -0,0 +1,471 @@ +[ + "Basics", + { + "input": "text/html;charset=gbk", + "output": "text/html;charset=gbk", + "navigable": true, + "encoding": "GBK", + "minimizedMIMEType": "text/html" + }, + { + "input": "TEXT/HTML;CHARSET=GBK", + "output": "text/html;charset=GBK", + "navigable": true, + "encoding": "GBK", + "minimizedMIMEType": "text/html" + }, + "Legacy comment syntax", + { + "input": "text/html;charset=gbk(", + "output": "text/html;charset=\"gbk(\"", + "navigable": true, + "encoding": null, + "minimizedMIMEType": "text/html" + }, + { + "input": "text/html;x=(;charset=gbk", + "output": "text/html;x=\"(\";charset=gbk", + "navigable": true, + "encoding": "GBK", + "minimizedMIMEType": "text/html" + }, + "Duplicate parameter", + { + "input": "text/html;charset=gbk;charset=windows-1255", + "output": "text/html;charset=gbk", + "navigable": true, + "encoding": "GBK", + "minimizedMIMEType": "text/html" + }, + { + "input": "text/html;charset=();charset=GBK", + "output": "text/html;charset=\"()\"", + "navigable": true, + "encoding": null, + "minimizedMIMEType": "text/html" + }, + "Spaces", + { + "input": "text/html;charset =gbk", + "output": "text/html", + "navigable": true, + "encoding": null, + "minimizedMIMEType": "text/html" + }, + { + "input": "text/html ;charset=gbk", + "output": "text/html;charset=gbk", + "navigable": true, + "encoding": "GBK", + "minimizedMIMEType": "text/html" + }, + { + "input": "text/html; charset=gbk", + "output": "text/html;charset=gbk", + "navigable": true, + "encoding": "GBK", + "minimizedMIMEType": "text/html" + }, + { + "input": "text/html;charset= gbk", + "output": "text/html;charset=\" gbk\"", + "navigable": true, + "encoding": "GBK", + "minimizedMIMEType": "text/html" + }, + { + "input": "text/html;charset= \"gbk\"", + "output": "text/html;charset=\" \\\"gbk\\\"\"", + "navigable": true, + "encoding": null, + "minimizedMIMEType": "text/html" + }, + "0x0B and 0x0C", + { + "input": "text/html;charset=\u000Bgbk", + "output": "text/html", + "navigable": true, + "encoding": null, + "minimizedMIMEType": "text/html" + }, + { + "input": "text/html;charset=\u000Cgbk", + "output": "text/html", + "navigable": true, + "encoding": null, + "minimizedMIMEType": "text/html" + }, + { + "input": "text/html;\u000Bcharset=gbk", + "output": "text/html", + "navigable": true, + "encoding": null, + "minimizedMIMEType": "text/html" + }, + { + "input": "text/html;\u000Ccharset=gbk", + "output": "text/html", + "navigable": true, + "encoding": null, + "minimizedMIMEType": "text/html" + }, + "Single quotes are a token, not a delimiter", + { + "input": "text/html;charset='gbk'", + "output": "text/html;charset='gbk'", + "navigable": true, + "encoding": null, + "minimizedMIMEType": "text/html" + }, + { + "input": "text/html;charset='gbk", + "output": "text/html;charset='gbk", + "navigable": true, + "encoding": null, + "minimizedMIMEType": "text/html" + }, + { + "input": "text/html;charset=gbk'", + "output": "text/html;charset=gbk'", + "navigable": true, + "encoding": null, + "minimizedMIMEType": "text/html" + }, + { + "input": "text/html;charset=';charset=GBK", + "output": "text/html;charset='", + "navigable": true, + "encoding": null, + "minimizedMIMEType": "text/html" + }, + "Invalid parameters", + { + "input": "text/html;test;charset=gbk", + "output": "text/html;charset=gbk", + "navigable": true, + "encoding": "GBK", + "minimizedMIMEType": "text/html" + }, + { + "input": "text/html;test=;charset=gbk", + "output": "text/html;charset=gbk", + "navigable": true, + "encoding": "GBK", + "minimizedMIMEType": "text/html" + }, + { + "input": "text/html;';charset=gbk", + "output": "text/html;charset=gbk", + "navigable": true, + "encoding": "GBK", + "minimizedMIMEType": "text/html" + }, + { + "input": "text/html;\";charset=gbk", + "output": "text/html;charset=gbk", + "navigable": true, + "encoding": "GBK", + "minimizedMIMEType": "text/html" + }, + { + "input": "text/html ; ; charset=gbk", + "output": "text/html;charset=gbk", + "navigable": true, + "encoding": "GBK", + "minimizedMIMEType": "text/html" + }, + { + "input": "text/html;;;;charset=gbk", + "output": "text/html;charset=gbk", + "navigable": true, + "encoding": "GBK", + "minimizedMIMEType": "text/html" + }, + { + "input": "text/html;charset= \"\u007F;charset=GBK", + "output": "text/html;charset=GBK", + "navigable": true, + "encoding": "GBK", + "minimizedMIMEType": "text/html" + }, + { + "input": "text/html;charset=\"\u007F;charset=foo\";charset=GBK", + "output": "text/html;charset=GBK", + "navigable": true, + "encoding": "GBK", + "minimizedMIMEType": "text/html" + }, + "Double quotes", + { + "input": "text/html;charset=\"gbk\"", + "output": "text/html;charset=gbk", + "navigable": true, + "encoding": "GBK", + "minimizedMIMEType": "text/html" + }, + { + "input": "text/html;charset=\"gbk", + "output": "text/html;charset=gbk", + "navigable": true, + "encoding": "GBK", + "minimizedMIMEType": "text/html" + }, + { + "input": "text/html;charset=gbk\"", + "output": "text/html;charset=\"gbk\\\"\"", + "navigable": true, + "encoding": null, + "minimizedMIMEType": "text/html" + }, + { + "input": "text/html;charset=\" gbk\"", + "output": "text/html;charset=\" gbk\"", + "navigable": true, + "encoding": "GBK", + "minimizedMIMEType": "text/html" + }, + { + "input": "text/html;charset=\"gbk \"", + "output": "text/html;charset=\"gbk \"", + "navigable": true, + "encoding": "GBK", + "minimizedMIMEType": "text/html" + }, + { + "input": "text/html;charset=\"\\ gbk\"", + "output": "text/html;charset=\" gbk\"", + "navigable": true, + "encoding": "GBK", + "minimizedMIMEType": "text/html" + }, + { + "input": "text/html;charset=\"\\g\\b\\k\"", + "output": "text/html;charset=gbk", + "navigable": true, + "encoding": "GBK", + "minimizedMIMEType": "text/html" + }, + { + "input": "text/html;charset=\"gbk\"x", + "output": "text/html;charset=gbk", + "navigable": true, + "encoding": "GBK", + "minimizedMIMEType": "text/html" + }, + { + "input": "text/html;charset=\"\";charset=GBK", + "output": "text/html;charset=\"\"", + "navigable": true, + "encoding": null, + "minimizedMIMEType": "text/html" + }, + { + "input": "text/html;charset=\";charset=GBK", + "output": "text/html;charset=\";charset=GBK\"", + "navigable": true, + "encoding": null, + "minimizedMIMEType": "text/html" + }, + "Unexpected code points", + { + "input": "text/html;charset={gbk}", + "output": "text/html;charset=\"{gbk}\"", + "navigable": true, + "encoding": null, + "minimizedMIMEType": "text/html" + }, + "Parameter name longer than 127", + { + "input": "text/html;0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789=x;charset=gbk", + "output": "text/html;0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789=x;charset=gbk", + "navigable": true, + "encoding": "GBK", + "minimizedMIMEType": "text/html" + }, + "type/subtype longer than 127", + { + "input": "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789/0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789", + "output": "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789/0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789", + "minimizedMIMEType": "" + }, + "Invalid names", + { + "input": "text/html;a]=bar;b[=bar;c=bar", + "output": "text/html;c=bar", + "minimizedMIMEType": "text/html" + }, + "Semicolons in value", + { + "input": "text/html;valid=\";\";foo=bar", + "output": "text/html;valid=\";\";foo=bar", + "minimizedMIMEType": "text/html" + }, + { + "input": "text/html;in]valid=\";asd=foo\";foo=bar", + "output": "text/html;foo=bar", + "minimizedMIMEType": "text/html" + }, + "Valid", + { + "input": "!#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz/!#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz;!#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz=!#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", + "output": "!#$%&'*+-.^_`|~0123456789abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz/!#$%&'*+-.^_`|~0123456789abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz;!#$%&'*+-.^_`|~0123456789abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz=!#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", + "minimizedMIMEType": "" + }, + { + "input": "x/x;x=\"\t !\\\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\u0080\u0081\u0082\u0083\u0084\u0085\u0086\u0087\u0088\u0089\u008A\u008B\u008C\u008D\u008E\u008F\u0090\u0091\u0092\u0093\u0094\u0095\u0096\u0097\u0098\u0099\u009A\u009B\u009C\u009D\u009E\u009F\u00A0\u00A1\u00A2\u00A3\u00A4\u00A5\u00A6\u00A7\u00A8\u00A9\u00AA\u00AB\u00AC\u00AD\u00AE\u00AF\u00B0\u00B1\u00B2\u00B3\u00B4\u00B5\u00B6\u00B7\u00B8\u00B9\u00BA\u00BB\u00BC\u00BD\u00BE\u00BF\u00C0\u00C1\u00C2\u00C3\u00C4\u00C5\u00C6\u00C7\u00C8\u00C9\u00CA\u00CB\u00CC\u00CD\u00CE\u00CF\u00D0\u00D1\u00D2\u00D3\u00D4\u00D5\u00D6\u00D7\u00D8\u00D9\u00DA\u00DB\u00DC\u00DD\u00DE\u00DF\u00E0\u00E1\u00E2\u00E3\u00E4\u00E5\u00E6\u00E7\u00E8\u00E9\u00EA\u00EB\u00EC\u00ED\u00EE\u00EF\u00F0\u00F1\u00F2\u00F3\u00F4\u00F5\u00F6\u00F7\u00F8\u00F9\u00FA\u00FB\u00FC\u00FD\u00FE\u00FF\"", + "output": "x/x;x=\"\t !\\\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\u0080\u0081\u0082\u0083\u0084\u0085\u0086\u0087\u0088\u0089\u008A\u008B\u008C\u008D\u008E\u008F\u0090\u0091\u0092\u0093\u0094\u0095\u0096\u0097\u0098\u0099\u009A\u009B\u009C\u009D\u009E\u009F\u00A0\u00A1\u00A2\u00A3\u00A4\u00A5\u00A6\u00A7\u00A8\u00A9\u00AA\u00AB\u00AC\u00AD\u00AE\u00AF\u00B0\u00B1\u00B2\u00B3\u00B4\u00B5\u00B6\u00B7\u00B8\u00B9\u00BA\u00BB\u00BC\u00BD\u00BE\u00BF\u00C0\u00C1\u00C2\u00C3\u00C4\u00C5\u00C6\u00C7\u00C8\u00C9\u00CA\u00CB\u00CC\u00CD\u00CE\u00CF\u00D0\u00D1\u00D2\u00D3\u00D4\u00D5\u00D6\u00D7\u00D8\u00D9\u00DA\u00DB\u00DC\u00DD\u00DE\u00DF\u00E0\u00E1\u00E2\u00E3\u00E4\u00E5\u00E6\u00E7\u00E8\u00E9\u00EA\u00EB\u00EC\u00ED\u00EE\u00EF\u00F0\u00F1\u00F2\u00F3\u00F4\u00F5\u00F6\u00F7\u00F8\u00F9\u00FA\u00FB\u00FC\u00FD\u00FE\u00FF\"", + "minimizedMIMEType": "" + }, + "End-of-file handling", + { + "input": "x/x;test", + "output": "x/x", + "minimizedMIMEType": "" + }, + { + "input": "x/x;test=\"\\", + "output": "x/x;test=\"\\\\\"", + "minimizedMIMEType": "" + }, + "Whitespace (not handled by generated-mime-types.json or above)", + { + "input": "x/x;x= ", + "output": "x/x", + "minimizedMIMEType": "" + }, + { + "input": "x/x;x=\t", + "output": "x/x", + "minimizedMIMEType": "" + }, + { + "input": "x/x\n\r\t ;x=x", + "output": "x/x;x=x", + "minimizedMIMEType": "" + }, + { + "input": "\n\r\t x/x;x=x\n\r\t ", + "output": "x/x;x=x", + "minimizedMIMEType": "" + }, + { + "input": "x/x;\n\r\t x=x\n\r\t ;x=y", + "output": "x/x;x=x", + "minimizedMIMEType": "" + }, + "Latin1", + { + "input": "text/html;test=\u00FF;charset=gbk", + "output": "text/html;test=\"\u00FF\";charset=gbk", + "navigable": true, + "encoding": "GBK", + "minimizedMIMEType": "text/html" + }, + ">Latin1", + { + "input": "x/x;test=\uFFFD;x=x", + "output": "x/x;x=x", + "minimizedMIMEType": "" + }, + "Failure", + { + "input": "\u000Bx/x", + "output": null, + "minimizedMIMEType": "" + }, + { + "input": "\u000Cx/x", + "output": null, + "minimizedMIMEType": "" + }, + { + "input": "x/x\u000B", + "output": null, + "minimizedMIMEType": "" + }, + { + "input": "x/x\u000C", + "output": null, + "minimizedMIMEType": "" + }, + { + "input": "", + "output": null, + "minimizedMIMEType": "" + }, + { + "input": "\t", + "output": null, + "minimizedMIMEType": "" + }, + { + "input": "/", + "output": null, + "minimizedMIMEType": "" + }, + { + "input": "bogus", + "output": null, + "minimizedMIMEType": "" + }, + { + "input": "bogus/", + "output": null, + "minimizedMIMEType": "" + }, + { + "input": "bogus/ ", + "output": null, + "minimizedMIMEType": "" + }, + { + "input": "bogus/bogus/;", + "output": null, + "minimizedMIMEType": "" + }, + { + "input": "", + "output": null, + "minimizedMIMEType": "" + }, + { + "input": "(/)", + "output": null, + "minimizedMIMEType": "" + }, + { + "input": "ÿ/ÿ", + "output": null, + "minimizedMIMEType": "" + }, + { + "input": "text/html(;doesnot=matter", + "output": null, + "minimizedMIMEType": "" + }, + { + "input": "{/}", + "output": null, + "minimizedMIMEType": "" + }, + { + "input": "\u0100/\u0100", + "output": null, + "minimizedMIMEType": "" + }, + { + "input": "text /html", + "output": null, + "minimizedMIMEType": "" + }, + { + "input": "text/ html", + "output": null, + "minimizedMIMEType": "" + }, + { + "input": "\"text/html\"", + "output": null, + "minimizedMIMEType": "" + } +]