summaryrefslogtreecommitdiffstats
path: root/third_party/rust/sfv
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
commit0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch)
treea31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /third_party/rust/sfv
parentInitial commit. (diff)
downloadfirefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz
firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/sfv')
-rw-r--r--third_party/rust/sfv/.cargo-checksum.json1
-rw-r--r--third_party/rust/sfv/Cargo.toml60
-rw-r--r--third_party/rust/sfv/LICENSE-APACHE201
-rw-r--r--third_party/rust/sfv/LICENSE-MIT27
-rw-r--r--third_party/rust/sfv/README.md20
-rw-r--r--third_party/rust/sfv/benches/bench.rs171
-rw-r--r--third_party/rust/sfv/src/lib.rs440
-rw-r--r--third_party/rust/sfv/src/parser.rs473
-rw-r--r--third_party/rust/sfv/src/ref_serializer.rs310
-rw-r--r--third_party/rust/sfv/src/serializer.rs319
-rw-r--r--third_party/rust/sfv/src/test_parser.rs850
-rw-r--r--third_party/rust/sfv/src/test_serializer.rs531
-rw-r--r--third_party/rust/sfv/src/utils.rs44
13 files changed, 3447 insertions, 0 deletions
diff --git a/third_party/rust/sfv/.cargo-checksum.json b/third_party/rust/sfv/.cargo-checksum.json
new file mode 100644
index 0000000000..b34463089b
--- /dev/null
+++ b/third_party/rust/sfv/.cargo-checksum.json
@@ -0,0 +1 @@
+{"files":{"Cargo.toml":"cf545d5d3384e757f96bffb58f35f1df6236a340e9aac13ae3398a8b9127fe30","LICENSE-APACHE":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2","LICENSE-MIT":"5318787a14e32720b1652f08a24408aaea67fdb5154ceda0f46a6069a0c5e5e3","README.md":"50a8b83ce705a34030957a3502c768674541220a5c2952a965f70c31faaed559","benches/bench.rs":"bbc60db4b542abb3738eba80f5c7c54ac39301ed5e48e2ae2a94cecfdb42e33f","src/lib.rs":"d6c579a4b35078c2f86d6515c957040eb54d9b7ecd65e74f90186d977986b358","src/parser.rs":"1c54f855a5bdd270b9b83b8cb95de670fdf14bce2154f683fba920b447adc94f","src/ref_serializer.rs":"8806ee50e2b2ae466a49788d7e972a47329c0e2c842d669673a152286e81c5d9","src/serializer.rs":"937541e0bdda7f3b043c55eb44ef2c431bdbf636498ce99af4ca47d773790178","src/test_parser.rs":"7a2728e7cbdcb1f3bb42e009045ec0dcfca241316a2aee4905925d4b1ce0bb3a","src/test_serializer.rs":"2419279c9a9a4f48952836d63f3822281c18691d86c146749a573c52a41d6ff0","src/utils.rs":"94c8f79f4747973819b9da2c1a9f6246bf3b5ea7450b376a98eb055f6acf8e73"},"package":"b4f1641177943b4e6faf7622463ae6dfe0f143eb88799e91e2e2e68ede568af5"} \ No newline at end of file
diff --git a/third_party/rust/sfv/Cargo.toml b/third_party/rust/sfv/Cargo.toml
new file mode 100644
index 0000000000..1827d69d5d
--- /dev/null
+++ b/third_party/rust/sfv/Cargo.toml
@@ -0,0 +1,60 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies.
+#
+# If you are reading this file be aware that the original Cargo.toml
+# will likely look very different (and much more reasonable).
+# See Cargo.toml.orig for the original contents.
+
+[package]
+edition = "2018"
+name = "sfv"
+version = "0.9.3"
+authors = ["Tania Batieva <yalyna.ts@gmail.com>"]
+exclude = [
+ "tests/**",
+ ".github/*",
+]
+description = """
+Structured Field Values for HTTP parser.
+Implementation of RFC 8941."""
+documentation = "https://docs.rs/sfv"
+readme = "README.md"
+keywords = [
+ "http-header",
+ "structured-header",
+]
+license = "MIT/Apache-2.0"
+repository = "https://github.com/undef1nd/sfv"
+
+[[bench]]
+name = "bench"
+harness = false
+
+[dependencies.data-encoding]
+version = "2.3.2"
+
+[dependencies.indexmap]
+version = "1.8.0"
+
+[dependencies.rust_decimal]
+version = "1.20.0"
+default-features = false
+
+[dev-dependencies.criterion]
+version = "0.4.0"
+
+[dev-dependencies.rust_decimal]
+version = "1.20.0"
+features = ["std"]
+default-features = false
+
+[dev-dependencies.serde]
+version = "1.0"
+features = ["derive"]
+
+[dev-dependencies.serde_json]
+version = "1.0"
diff --git a/third_party/rust/sfv/LICENSE-APACHE b/third_party/rust/sfv/LICENSE-APACHE
new file mode 100644
index 0000000000..16fe87b06e
--- /dev/null
+++ b/third_party/rust/sfv/LICENSE-APACHE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/third_party/rust/sfv/LICENSE-MIT b/third_party/rust/sfv/LICENSE-MIT
new file mode 100644
index 0000000000..1fd78f4072
--- /dev/null
+++ b/third_party/rust/sfv/LICENSE-MIT
@@ -0,0 +1,27 @@
+MIT License
+
+Copyright (c) 2020 Tania Batieva
+
+Permission is hereby granted, free of charge, to any
+person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the
+Software without restriction, including without
+limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions
+of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
diff --git a/third_party/rust/sfv/README.md b/third_party/rust/sfv/README.md
new file mode 100644
index 0000000000..2a781b611f
--- /dev/null
+++ b/third_party/rust/sfv/README.md
@@ -0,0 +1,20 @@
+[![License](https://img.shields.io/crates/l/sfv)](LICENSE-MIT)
+![Build Status](https://img.shields.io/github/actions/workflow/status/undef1nd/sfv/ci-workflow.yml)
+[![Version](https://img.shields.io/crates/v/sfv)](https://crates.io/crates/sfv)
+[![Docs](https://img.shields.io/docsrs/sfv?color=white)](https://docs.rs/sfv)
+
+
+# Structured Field Values for HTTP
+
+`sfv` crate is an implementation of *Structured Field Values for HTTP* as specified in [RFC 8941](https://httpwg.org/specs/rfc8941.html) for parsing and serializing HTTP field values (also known as "structured headers" or "structured trailers").
+
+It also exposes a set of types that might be useful for defining new structured fields.
+
+<br>
+
+#### License
+
+<sup>
+Licensed under either of <a href="LICENSE-APACHE">Apache License, Version
+2.0</a> or <a href="LICENSE-MIT">MIT license</a> at your option.
+</sup>
diff --git a/third_party/rust/sfv/benches/bench.rs b/third_party/rust/sfv/benches/bench.rs
new file mode 100644
index 0000000000..3e924c1f32
--- /dev/null
+++ b/third_party/rust/sfv/benches/bench.rs
@@ -0,0 +1,171 @@
+#[macro_use]
+extern crate criterion;
+
+use criterion::{BenchmarkId, Criterion};
+use rust_decimal::prelude::FromPrimitive;
+use sfv::{BareItem, Decimal, Parser, SerializeValue};
+use sfv::{RefBareItem, RefDictSerializer, RefItemSerializer, RefListSerializer};
+
+criterion_main!(parsing, serializing, ref_serializing);
+
+criterion_group!(parsing, parsing_item, parsing_list, parsing_dict);
+
+fn parsing_item(c: &mut Criterion) {
+ let fixture =
+ "c29tZXZlcnlsb25nc3RyaW5ndmFsdWVyZXByZXNlbnRlZGFzYnl0ZXNhbnNvbWVvdGhlcmxvbmdsaW5l";
+ c.bench_with_input(
+ BenchmarkId::new("parsing_item", fixture),
+ &fixture,
+ move |bench, &input| {
+ bench.iter(|| Parser::parse_item(input.as_bytes()).unwrap());
+ },
+ );
+}
+
+fn parsing_list(c: &mut Criterion) {
+ let fixture = "a, abcdefghigklmnoprst, 123456785686457, 99999999999.999, (), (\"somelongstringvalue\" \"anotherlongstringvalue\";key=:c29tZXZlciBsb25nc3RyaW5ndmFsdWVyZXByZXNlbnRlZGFzYnl0ZXM: 145)";
+ c.bench_with_input(
+ BenchmarkId::new("parsing_list", fixture),
+ &fixture,
+ move |bench, &input| {
+ bench.iter(|| Parser::parse_list(input.as_bytes()).unwrap());
+ },
+ );
+}
+
+fn parsing_dict(c: &mut Criterion) {
+ let fixture = "a, dict_key2=abcdefghigklmnoprst, dict_key3=123456785686457, dict_key4=(\"inner-list-member\" :aW5uZXItbGlzdC1tZW1iZXI=:);key=aW5uZXItbGlzdC1wYXJhbWV0ZXJz";
+ c.bench_with_input(
+ BenchmarkId::new("parsing_dict", fixture),
+ &fixture,
+ move |bench, &input| {
+ bench.iter(|| Parser::parse_dictionary(input.as_bytes()).unwrap());
+ },
+ );
+}
+
+criterion_group!(
+ serializing,
+ serializing_item,
+ serializing_list,
+ serializing_dict
+);
+
+fn serializing_item(c: &mut Criterion) {
+ let fixture =
+ "c29tZXZlcnlsb25nc3RyaW5ndmFsdWVyZXByZXNlbnRlZGFzYnl0ZXNhbnNvbWVvdGhlcmxvbmdsaW5l";
+ c.bench_with_input(
+ BenchmarkId::new("serializing_item", fixture),
+ &fixture,
+ move |bench, &input| {
+ let parsed_item = Parser::parse_item(input.as_bytes()).unwrap();
+ bench.iter(|| parsed_item.serialize_value().unwrap());
+ },
+ );
+}
+
+fn serializing_list(c: &mut Criterion) {
+ let fixture = "a, abcdefghigklmnoprst, 123456785686457, 99999999999.999, (), (\"somelongstringvalue\" \"anotherlongstringvalue\";key=:c29tZXZlciBsb25nc3RyaW5ndmFsdWVyZXByZXNlbnRlZGFzYnl0ZXM: 145)";
+ c.bench_with_input(
+ BenchmarkId::new("serializing_list", fixture),
+ &fixture,
+ move |bench, &input| {
+ let parsed_list = Parser::parse_list(input.as_bytes()).unwrap();
+ bench.iter(|| parsed_list.serialize_value().unwrap());
+ },
+ );
+}
+
+fn serializing_dict(c: &mut Criterion) {
+ let fixture = "a, dict_key2=abcdefghigklmnoprst, dict_key3=123456785686457, dict_key4=(\"inner-list-member\" :aW5uZXItbGlzdC1tZW1iZXI=:);key=aW5uZXItbGlzdC1wYXJhbWV0ZXJz";
+ c.bench_with_input(
+ BenchmarkId::new("serializing_dict", fixture),
+ &fixture,
+ move |bench, &input| {
+ let parsed_dict = Parser::parse_dictionary(input.as_bytes()).unwrap();
+ bench.iter(|| parsed_dict.serialize_value().unwrap());
+ },
+ );
+}
+
+criterion_group!(
+ ref_serializing,
+ serializing_ref_item,
+ serializing_ref_list,
+ serializing_ref_dict
+);
+
+fn serializing_ref_item(c: &mut Criterion) {
+ let fixture =
+ "c29tZXZlcnlsb25nc3RyaW5ndmFsdWVyZXByZXNlbnRlZGFzYnl0ZXNhbnNvbWVvdGhlcmxvbmdsaW5l";
+ c.bench_with_input(
+ BenchmarkId::new("serializing_ref_item", fixture),
+ &fixture,
+ move |bench, &input| {
+ bench.iter(|| {
+ let mut output = String::new();
+ let ser = RefItemSerializer::new(&mut output);
+ ser.bare_item(&RefBareItem::ByteSeq(input.as_bytes()))
+ .unwrap();
+ });
+ },
+ );
+}
+
+fn serializing_ref_list(c: &mut Criterion) {
+ c.bench_function("serializing_ref_list", move |bench| {
+ bench.iter(|| {
+ let mut output = String::new();
+ let ser = RefListSerializer::new(&mut output);
+ ser.bare_item(&RefBareItem::Token("a"))
+ .unwrap()
+ .bare_item(&RefBareItem::Token("abcdefghigklmnoprst"))
+ .unwrap()
+ .bare_item(&RefBareItem::Integer(123456785686457))
+ .unwrap()
+ .bare_item(&RefBareItem::Decimal(
+ Decimal::from_f64(99999999999.999).unwrap(),
+ ))
+ .unwrap()
+ .open_inner_list()
+ .close_inner_list()
+ .open_inner_list()
+ .inner_list_bare_item(&RefBareItem::String("somelongstringvalue"))
+ .unwrap()
+ .inner_list_bare_item(&RefBareItem::String("anotherlongstringvalue"))
+ .unwrap()
+ .inner_list_parameter(
+ "key",
+ &RefBareItem::ByteSeq("somever longstringvaluerepresentedasbytes".as_bytes()),
+ )
+ .unwrap()
+ .inner_list_bare_item(&RefBareItem::Integer(145))
+ .unwrap()
+ .close_inner_list();
+ });
+ });
+}
+
+fn serializing_ref_dict(c: &mut Criterion) {
+ c.bench_function("serializing_ref_dict", move |bench| {
+ bench.iter(|| {
+ let mut output = String::new();
+ RefDictSerializer::new(&mut output)
+ .bare_item_member("a", &RefBareItem::Boolean(true))
+ .unwrap()
+ .bare_item_member("dict_key2", &RefBareItem::Token("abcdefghigklmnoprst"))
+ .unwrap()
+ .bare_item_member("dict_key3", &RefBareItem::Integer(123456785686457))
+ .unwrap()
+ .open_inner_list("dict_key4")
+ .unwrap()
+ .inner_list_bare_item(&RefBareItem::String("inner-list-member"))
+ .unwrap()
+ .inner_list_bare_item(&RefBareItem::ByteSeq("inner-list-member".as_bytes()))
+ .unwrap()
+ .close_inner_list()
+ .parameter("key", &RefBareItem::Token("aW5uZXItbGlzdC1wYXJhbWV0ZXJz"))
+ .unwrap();
+ });
+ });
+}
diff --git a/third_party/rust/sfv/src/lib.rs b/third_party/rust/sfv/src/lib.rs
new file mode 100644
index 0000000000..f7aebfcdd2
--- /dev/null
+++ b/third_party/rust/sfv/src/lib.rs
@@ -0,0 +1,440 @@
+/*!
+`sfv` crate is an implementation of *Structured Field Values for HTTP* as specified in [RFC 8941](https://httpwg.org/specs/rfc8941.html) for parsing and serializing HTTP field values.
+It also exposes a set of types that might be useful for defining new structured fields.
+
+# Data Structures
+
+There are three types of structured fields:
+
+- `Item` - can be an `Integer`, `Decimal`, `String`, `Token`, `Byte Sequence`, or `Boolean`. It can have associated `Parameters`.
+- `List` - array of zero or more members, each of which can be an `Item` or an `InnerList`, both of which can be `Parameterized`.
+- `Dictionary` - ordered map of name-value pairs, where the names are short textual strings and the values are `Items` or arrays of `Items` (represented with `InnerList`), both of which can be `Parameterized`. There can be zero or more members, and their names are unique in the scope of the `Dictionary` they occur within.
+
+There's also a few primitive types used to construct structured field values:
+- `BareItem` used as `Item`'s value or as a parameter value in `Parameters`.
+- `Parameters` are an ordered map of key-value pairs that are associated with an `Item` or `InnerList`. The keys are unique within the scope the `Parameters` they occur within, and the values are `BareItem`.
+- `InnerList` is an array of zero or more `Items`. Can have `Parameters`.
+- `ListEntry` represents either `Item` or `InnerList` as a member of `List` or as member-value in `Dictionary`.
+
+# Examples
+
+### Parsing
+
+```
+use sfv::Parser;
+
+// Parsing structured field value of Item type.
+let item_header_input = "12.445;foo=bar";
+let item = Parser::parse_item(item_header_input.as_bytes());
+assert!(item.is_ok());
+println!("{:#?}", item);
+
+// Parsing structured field value of List type.
+let list_header_input = "1;a=tok, (\"foo\" \"bar\");baz, ()";
+let list = Parser::parse_list(list_header_input.as_bytes());
+assert!(list.is_ok());
+println!("{:#?}", list);
+
+// Parsing structured field value of Dictionary type.
+let dict_header_input = "a=?0, b, c; foo=bar, rating=1.5, fruits=(apple pear)";
+let dict = Parser::parse_dictionary(dict_header_input.as_bytes());
+assert!(dict.is_ok());
+println!("{:#?}", dict);
+```
+
+### Getting Parsed Value Members
+```
+use sfv::*;
+
+let dict_header = "u=2, n=(* foo 2)";
+ let dict = Parser::parse_dictionary(dict_header.as_bytes()).unwrap();
+
+ // Case 1 - handling value if it's an Item of Integer type
+ let u_val = match dict.get("u") {
+ Some(ListEntry::Item(item)) => item.bare_item.as_int(),
+ _ => None,
+ };
+
+ if let Some(u_val) = u_val {
+ println!("{}", u_val);
+ }
+
+ // Case 2 - matching on all possible types
+ match dict.get("u") {
+ Some(ListEntry::Item(item)) => match &item.bare_item {
+ BareItem::Token(val) => {
+ // do something if it's a Token
+ println!("{}", val);
+ }
+ BareItem::Integer(val) => {
+ // do something if it's an Integer
+ println!("{}", val);
+ }
+ BareItem::Boolean(val) => {
+ // do something if it's a Boolean
+ println!("{}", val);
+ }
+ BareItem::Decimal(val) => {
+ // do something if it's a Decimal
+ println!("{}", val);
+ }
+ BareItem::String(val) => {
+ // do something if it's a String
+ println!("{}", val);
+ }
+ BareItem::ByteSeq(val) => {
+ // do something if it's a ByteSeq
+ println!("{:?}", val);
+ }
+ },
+ Some(ListEntry::InnerList(inner_list)) => {
+ // do something if it's an InnerList
+ println!("{:?}", inner_list.items);
+ }
+ None => panic!("key not found"),
+ }
+```
+
+### Structured Field Value Construction and Serialization
+Creates `Item` with empty parameters:
+```
+use sfv::{Item, BareItem, SerializeValue};
+
+let str_item = Item::new(BareItem::String(String::from("foo")));
+assert_eq!(str_item.serialize_value().unwrap(), "\"foo\"");
+```
+
+
+Creates `Item` field value with parameters:
+```
+use sfv::{Item, BareItem, SerializeValue, Parameters, Decimal, FromPrimitive};
+
+let mut params = Parameters::new();
+let decimal = Decimal::from_f64(13.45655).unwrap();
+params.insert("key".into(), BareItem::Decimal(decimal));
+let int_item = Item::with_params(BareItem::Integer(99), params);
+assert_eq!(int_item.serialize_value().unwrap(), "99;key=13.457");
+```
+
+Creates `List` field value with `Item` and parametrized `InnerList` as members:
+```
+use sfv::{Item, BareItem, InnerList, List, SerializeValue, Parameters};
+
+let tok_item = BareItem::Token("tok".into());
+
+// Creates Item.
+let str_item = Item::new(BareItem::String(String::from("foo")));
+
+// Creates InnerList members.
+let mut int_item_params = Parameters::new();
+int_item_params.insert("key".into(), BareItem::Boolean(false));
+let int_item = Item::with_params(BareItem::Integer(99), int_item_params);
+
+// Creates InnerList.
+let mut inner_list_params = Parameters::new();
+inner_list_params.insert("bar".into(), BareItem::Boolean(true));
+let inner_list = InnerList::with_params(vec![int_item, str_item], inner_list_params);
+
+
+let list: List = vec![Item::new(tok_item).into(), inner_list.into()];
+assert_eq!(
+ list.serialize_value().unwrap(),
+ "tok, (99;key=?0 \"foo\");bar"
+);
+```
+
+Creates `Dictionary` field value:
+```
+use sfv::{Parser, Item, BareItem, SerializeValue, ParseValue, Dictionary};
+
+let member_value1 = Item::new(BareItem::String(String::from("apple")));
+let member_value2 = Item::new(BareItem::Boolean(true));
+let member_value3 = Item::new(BareItem::Boolean(false));
+
+let mut dict = Dictionary::new();
+dict.insert("key1".into(), member_value1.into());
+dict.insert("key2".into(), member_value2.into());
+dict.insert("key3".into(), member_value3.into());
+
+assert_eq!(
+ dict.serialize_value().unwrap(),
+ "key1=\"apple\", key2, key3=?0"
+);
+
+```
+*/
+
+mod parser;
+mod ref_serializer;
+mod serializer;
+mod utils;
+
+#[cfg(test)]
+mod test_parser;
+#[cfg(test)]
+mod test_serializer;
+use indexmap::IndexMap;
+
+pub use rust_decimal::{
+ prelude::{FromPrimitive, FromStr},
+ Decimal,
+};
+
+pub use parser::{ParseMore, ParseValue, Parser};
+pub use ref_serializer::{RefDictSerializer, RefItemSerializer, RefListSerializer};
+pub use serializer::SerializeValue;
+
+type SFVResult<T> = std::result::Result<T, &'static str>;
+
+/// Represents `Item` type structured field value.
+/// Can be used as a member of `List` or `Dictionary`.
+// sf-item = bare-item parameters
+// bare-item = sf-integer / sf-decimal / sf-string / sf-token
+// / sf-binary / sf-boolean
+#[derive(Debug, PartialEq, Clone)]
+pub struct Item {
+ /// Value of `Item`.
+ pub bare_item: BareItem,
+ /// `Item`'s associated parameters. Can be empty.
+ pub params: Parameters,
+}
+
+impl Item {
+ /// Returns new `Item` with empty `Parameters`.
+ pub fn new(bare_item: BareItem) -> Item {
+ Item {
+ bare_item,
+ params: Parameters::new(),
+ }
+ }
+ /// Returns new `Item` with specified `Parameters`.
+ pub fn with_params(bare_item: BareItem, params: Parameters) -> Item {
+ Item { bare_item, params }
+ }
+}
+
+/// Represents `Dictionary` type structured field value.
+// sf-dictionary = dict-member *( OWS "," OWS dict-member )
+// dict-member = member-name [ "=" member-value ]
+// member-name = key
+// member-value = sf-item / inner-list
+pub type Dictionary = IndexMap<String, ListEntry>;
+
+/// Represents `List` type structured field value.
+// sf-list = list-member *( OWS "," OWS list-member )
+// list-member = sf-item / inner-list
+pub type List = Vec<ListEntry>;
+
+/// Parameters of `Item` or `InnerList`.
+// parameters = *( ";" *SP parameter )
+// parameter = param-name [ "=" param-value ]
+// param-name = key
+// key = ( lcalpha / "*" )
+// *( lcalpha / DIGIT / "_" / "-" / "." / "*" )
+// lcalpha = %x61-7A ; a-z
+// param-value = bare-item
+pub type Parameters = IndexMap<String, BareItem>;
+
+/// Represents a member of `List` or `Dictionary` structured field value.
+#[derive(Debug, PartialEq, Clone)]
+pub enum ListEntry {
+ /// Member of `Item` type.
+ Item(Item),
+ /// Member of `InnerList` (array of `Items`) type.
+ InnerList(InnerList),
+}
+
+impl From<Item> for ListEntry {
+ fn from(item: Item) -> Self {
+ ListEntry::Item(item)
+ }
+}
+
+impl From<InnerList> for ListEntry {
+ fn from(item: InnerList) -> Self {
+ ListEntry::InnerList(item)
+ }
+}
+
+/// Array of `Items` with associated `Parameters`.
+// inner-list = "(" *SP [ sf-item *( 1*SP sf-item ) *SP ] ")"
+// parameters
+#[derive(Debug, PartialEq, Clone)]
+pub struct InnerList {
+ /// `Items` that `InnerList` contains. Can be empty.
+ pub items: Vec<Item>,
+ /// `InnerList`'s associated parameters. Can be empty.
+ pub params: Parameters,
+}
+
+impl InnerList {
+ /// Returns new `InnerList` with empty `Parameters`.
+ pub fn new(items: Vec<Item>) -> InnerList {
+ InnerList {
+ items,
+ params: Parameters::new(),
+ }
+ }
+
+ /// Returns new `InnerList` with specified `Parameters`.
+ pub fn with_params(items: Vec<Item>, params: Parameters) -> InnerList {
+ InnerList { items, params }
+ }
+}
+
+/// `BareItem` type is used to construct `Items` or `Parameters` values.
+#[derive(Debug, PartialEq, Clone)]
+pub enum BareItem {
+ /// Decimal number
+ // sf-decimal = ["-"] 1*12DIGIT "." 1*3DIGIT
+ Decimal(Decimal),
+ /// Integer number
+ // sf-integer = ["-"] 1*15DIGIT
+ Integer(i64),
+ // sf-string = DQUOTE *chr DQUOTE
+ // chr = unescaped / escaped
+ // unescaped = %x20-21 / %x23-5B / %x5D-7E
+ // escaped = "\" ( DQUOTE / "\" )
+ String(String),
+ // ":" *(base64) ":"
+ // base64 = ALPHA / DIGIT / "+" / "/" / "="
+ ByteSeq(Vec<u8>),
+ // sf-boolean = "?" boolean
+ // boolean = "0" / "1"
+ Boolean(bool),
+ // sf-token = ( ALPHA / "*" ) *( tchar / ":" / "/" )
+ Token(String),
+}
+
+impl BareItem {
+ /// If `BareItem` is a decimal, returns `Decimal`, otherwise returns `None`.
+ /// ```
+ /// # use sfv::{BareItem, Decimal, FromPrimitive};
+ /// let decimal_number = Decimal::from_f64(415.566).unwrap();
+ /// let bare_item: BareItem = decimal_number.into();
+ /// assert_eq!(bare_item.as_decimal().unwrap(), decimal_number);
+ /// ```
+ pub fn as_decimal(&self) -> Option<Decimal> {
+ match *self {
+ BareItem::Decimal(val) => Some(val),
+ _ => None,
+ }
+ }
+ /// If `BareItem` is an integer, returns `i64`, otherwise returns `None`.
+ /// ```
+ /// # use sfv::BareItem;
+ /// let bare_item: BareItem = 100.into();
+ /// assert_eq!(bare_item.as_int().unwrap(), 100);
+ /// ```
+ pub fn as_int(&self) -> Option<i64> {
+ match *self {
+ BareItem::Integer(val) => Some(val),
+ _ => None,
+ }
+ }
+ /// If `BareItem` is `String`, returns `&str`, otherwise returns `None`.
+ /// ```
+ /// # use sfv::BareItem;
+ /// let bare_item = BareItem::String("foo".into());
+ /// assert_eq!(bare_item.as_str().unwrap(), "foo");
+ /// ```
+ pub fn as_str(&self) -> Option<&str> {
+ match *self {
+ BareItem::String(ref val) => Some(val),
+ _ => None,
+ }
+ }
+ /// If `BareItem` is a `ByteSeq`, returns `&Vec<u8>`, otherwise returns `None`.
+ /// ```
+ /// # use sfv::BareItem;
+ /// let bare_item = BareItem::ByteSeq("foo".to_owned().into_bytes());
+ /// assert_eq!(bare_item.as_byte_seq().unwrap().as_slice(), "foo".as_bytes());
+ /// ```
+ pub fn as_byte_seq(&self) -> Option<&Vec<u8>> {
+ match *self {
+ BareItem::ByteSeq(ref val) => Some(val),
+ _ => None,
+ }
+ }
+ /// If `BareItem` is a `Boolean`, returns `bool`, otherwise returns `None`.
+ /// ```
+ /// # use sfv::{BareItem, Decimal, FromPrimitive};
+ /// let bare_item = BareItem::Boolean(true);
+ /// assert_eq!(bare_item.as_bool().unwrap(), true);
+ /// ```
+ pub fn as_bool(&self) -> Option<bool> {
+ match *self {
+ BareItem::Boolean(val) => Some(val),
+ _ => None,
+ }
+ }
+ /// If `BareItem` is a `Token`, returns `&str`, otherwise returns `None`.
+ /// ```
+ /// use sfv::BareItem;
+ ///
+ /// let bare_item = BareItem::Token("*bar".into());
+ /// assert_eq!(bare_item.as_token().unwrap(), "*bar");
+ /// ```
+ pub fn as_token(&self) -> Option<&str> {
+ match *self {
+ BareItem::Token(ref val) => Some(val),
+ _ => None,
+ }
+ }
+}
+
+impl From<i64> for BareItem {
+ /// Converts `i64` into `BareItem::Integer`.
+ /// ```
+ /// # use sfv::BareItem;
+ /// let bare_item: BareItem = 456.into();
+ /// assert_eq!(bare_item.as_int().unwrap(), 456);
+ /// ```
+ fn from(item: i64) -> Self {
+ BareItem::Integer(item)
+ }
+}
+
+impl From<Decimal> for BareItem {
+ /// Converts `Decimal` into `BareItem::Decimal`.
+ /// ```
+ /// # use sfv::{BareItem, Decimal, FromPrimitive};
+ /// let decimal_number = Decimal::from_f64(48.01).unwrap();
+ /// let bare_item: BareItem = decimal_number.into();
+ /// assert_eq!(bare_item.as_decimal().unwrap(), decimal_number);
+ /// ```
+ fn from(item: Decimal) -> Self {
+ BareItem::Decimal(item)
+ }
+}
+
+#[derive(Debug, PartialEq)]
+pub(crate) enum Num {
+ Decimal(Decimal),
+ Integer(i64),
+}
+
+/// Similar to `BareItem`, but used to serialize values via `RefItemSerializer`, `RefListSerializer`, `RefDictSerializer`.
+#[derive(Debug, PartialEq, Clone)]
+pub enum RefBareItem<'a> {
+ Integer(i64),
+ Decimal(Decimal),
+ String(&'a str),
+ ByteSeq(&'a [u8]),
+ Boolean(bool),
+ Token(&'a str),
+}
+
+impl BareItem {
+ /// Converts `BareItem` into `RefBareItem`.
+ fn to_ref_bare_item(&self) -> RefBareItem {
+ match self {
+ BareItem::Integer(val) => RefBareItem::Integer(*val),
+ BareItem::Decimal(val) => RefBareItem::Decimal(*val),
+ BareItem::String(val) => RefBareItem::String(val),
+ BareItem::ByteSeq(val) => RefBareItem::ByteSeq(val.as_slice()),
+ BareItem::Boolean(val) => RefBareItem::Boolean(*val),
+ BareItem::Token(val) => RefBareItem::Token(val),
+ }
+ }
+}
diff --git a/third_party/rust/sfv/src/parser.rs b/third_party/rust/sfv/src/parser.rs
new file mode 100644
index 0000000000..943380f279
--- /dev/null
+++ b/third_party/rust/sfv/src/parser.rs
@@ -0,0 +1,473 @@
+use crate::utils;
+use crate::{
+ BareItem, Decimal, Dictionary, FromStr, InnerList, Item, List, ListEntry, Num, Parameters,
+ SFVResult,
+};
+use std::iter::Peekable;
+use std::str::{from_utf8, Chars};
+
+/// Implements parsing logic for each structured field value type.
+pub trait ParseValue {
+ /// This method should not be used for parsing input into structured field value.
+ /// Use `Parser::parse_item`, `Parser::parse_list` or `Parsers::parse_dictionary` for that.
+ fn parse(input_chars: &mut Peekable<Chars>) -> SFVResult<Self>
+ where
+ Self: Sized;
+}
+
+/// If structured field value of List or Dictionary type is split into multiple lines,
+/// allows to parse more lines and merge them into already existing structure field value.
+pub trait ParseMore {
+ /// If structured field value is split across lines,
+ /// parses and merges next line into a single structured field value.
+ /// # Examples
+ /// ```
+ /// # use sfv::{Parser, SerializeValue, ParseMore};
+ ///
+ /// let mut list_field = Parser::parse_list("11, (12 13)".as_bytes()).unwrap();
+ /// list_field.parse_more("\"foo\", \"bar\"".as_bytes()).unwrap();
+ ///
+ /// assert_eq!(list_field.serialize_value().unwrap(), "11, (12 13), \"foo\", \"bar\"");
+ fn parse_more(&mut self, input_bytes: &[u8]) -> SFVResult<()>
+ where
+ Self: Sized;
+}
+
+impl ParseValue for Item {
+ fn parse(input_chars: &mut Peekable<Chars>) -> SFVResult<Item> {
+ // https://httpwg.org/specs/rfc8941.html#parse-item
+ let bare_item = Parser::parse_bare_item(input_chars)?;
+ let params = Parser::parse_parameters(input_chars)?;
+
+ Ok(Item { bare_item, params })
+ }
+}
+
+impl ParseValue for List {
+ fn parse(input_chars: &mut Peekable<Chars>) -> SFVResult<List> {
+ // https://httpwg.org/specs/rfc8941.html#parse-list
+ // List represents an array of (item_or_inner_list, parameters)
+
+ let mut members = vec![];
+
+ while input_chars.peek().is_some() {
+ members.push(Parser::parse_list_entry(input_chars)?);
+
+ utils::consume_ows_chars(input_chars);
+
+ if input_chars.peek().is_none() {
+ return Ok(members);
+ }
+
+ if let Some(c) = input_chars.next() {
+ if c != ',' {
+ return Err("parse_list: trailing characters after list member");
+ }
+ }
+
+ utils::consume_ows_chars(input_chars);
+
+ if input_chars.peek().is_none() {
+ return Err("parse_list: trailing comma");
+ }
+ }
+
+ Ok(members)
+ }
+}
+
+impl ParseValue for Dictionary {
+ fn parse(input_chars: &mut Peekable<Chars>) -> SFVResult<Dictionary> {
+ let mut dict = Dictionary::new();
+
+ while input_chars.peek().is_some() {
+ let this_key = Parser::parse_key(input_chars)?;
+
+ if let Some('=') = input_chars.peek() {
+ input_chars.next();
+ let member = Parser::parse_list_entry(input_chars)?;
+ dict.insert(this_key, member);
+ } else {
+ let value = true;
+ let params = Parser::parse_parameters(input_chars)?;
+ let member = Item {
+ bare_item: BareItem::Boolean(value),
+ params,
+ };
+ dict.insert(this_key, member.into());
+ }
+
+ utils::consume_ows_chars(input_chars);
+
+ if input_chars.peek().is_none() {
+ return Ok(dict);
+ }
+
+ if let Some(c) = input_chars.next() {
+ if c != ',' {
+ return Err("parse_dict: trailing characters after dictionary member");
+ }
+ }
+
+ utils::consume_ows_chars(input_chars);
+
+ if input_chars.peek().is_none() {
+ return Err("parse_dict: trailing comma");
+ }
+ }
+ Ok(dict)
+ }
+}
+
+impl ParseMore for List {
+ fn parse_more(&mut self, input_bytes: &[u8]) -> SFVResult<()> {
+ let parsed_list = Parser::parse_list(input_bytes)?;
+ self.extend(parsed_list);
+ Ok(())
+ }
+}
+
+impl ParseMore for Dictionary {
+ fn parse_more(&mut self, input_bytes: &[u8]) -> SFVResult<()> {
+ let parsed_dict = Parser::parse_dictionary(input_bytes)?;
+ self.extend(parsed_dict);
+ Ok(())
+ }
+}
+
+/// Exposes methods for parsing input into structured field value.
+pub struct Parser;
+
+impl Parser {
+ /// Parses input into structured field value of Dictionary type
+ pub fn parse_dictionary(input_bytes: &[u8]) -> SFVResult<Dictionary> {
+ Self::parse::<Dictionary>(input_bytes)
+ }
+
+ /// Parses input into structured field value of List type
+ pub fn parse_list(input_bytes: &[u8]) -> SFVResult<List> {
+ Self::parse::<List>(input_bytes)
+ }
+
+ /// Parses input into structured field value of Item type
+ pub fn parse_item(input_bytes: &[u8]) -> SFVResult<Item> {
+ Self::parse::<Item>(input_bytes)
+ }
+
+ // Generic parse method for checking input before parsing
+ // and handling trailing text error
+ fn parse<T: ParseValue>(input_bytes: &[u8]) -> SFVResult<T> {
+ // https://httpwg.org/specs/rfc8941.html#text-parse
+ if !input_bytes.is_ascii() {
+ return Err("parse: non-ascii characters in input");
+ }
+
+ let mut input_chars = from_utf8(input_bytes)
+ .map_err(|_| "parse: conversion from bytes to str failed")?
+ .chars()
+ .peekable();
+ utils::consume_sp_chars(&mut input_chars);
+
+ let output = T::parse(&mut input_chars)?;
+
+ utils::consume_sp_chars(&mut input_chars);
+
+ if input_chars.next().is_some() {
+ return Err("parse: trailing characters after parsed value");
+ };
+ Ok(output)
+ }
+
+ fn parse_list_entry(input_chars: &mut Peekable<Chars>) -> SFVResult<ListEntry> {
+ // https://httpwg.org/specs/rfc8941.html#parse-item-or-list
+ // ListEntry represents a tuple (item_or_inner_list, parameters)
+
+ match input_chars.peek() {
+ Some('(') => {
+ let parsed = Self::parse_inner_list(input_chars)?;
+ Ok(ListEntry::InnerList(parsed))
+ }
+ _ => {
+ let parsed = Item::parse(input_chars)?;
+ Ok(ListEntry::Item(parsed))
+ }
+ }
+ }
+
+ pub(crate) fn parse_inner_list(input_chars: &mut Peekable<Chars>) -> SFVResult<InnerList> {
+ // https://httpwg.org/specs/rfc8941.html#parse-innerlist
+
+ if Some('(') != input_chars.next() {
+ return Err("parse_inner_list: input does not start with '('");
+ }
+
+ let mut inner_list = Vec::new();
+ while input_chars.peek().is_some() {
+ utils::consume_sp_chars(input_chars);
+
+ if Some(&')') == input_chars.peek() {
+ input_chars.next();
+ let params = Self::parse_parameters(input_chars)?;
+ return Ok(InnerList {
+ items: inner_list,
+ params,
+ });
+ }
+
+ let parsed_item = Item::parse(input_chars)?;
+ inner_list.push(parsed_item);
+
+ if let Some(c) = input_chars.peek() {
+ if c != &' ' && c != &')' {
+ return Err("parse_inner_list: bad delimitation");
+ }
+ }
+ }
+
+ Err("parse_inner_list: the end of the inner list was not found")
+ }
+
+ pub(crate) fn parse_bare_item(input_chars: &mut Peekable<Chars>) -> SFVResult<BareItem> {
+ // https://httpwg.org/specs/rfc8941.html#parse-bare-item
+ if input_chars.peek().is_none() {
+ return Err("parse_bare_item: empty item");
+ }
+
+ match input_chars.peek() {
+ Some(&'?') => Ok(BareItem::Boolean(Self::parse_bool(input_chars)?)),
+ Some(&'"') => Ok(BareItem::String(Self::parse_string(input_chars)?)),
+ Some(&':') => Ok(BareItem::ByteSeq(Self::parse_byte_sequence(input_chars)?)),
+ Some(&c) if c == '*' || c.is_ascii_alphabetic() => {
+ Ok(BareItem::Token(Self::parse_token(input_chars)?))
+ }
+ Some(&c) if c == '-' || c.is_ascii_digit() => match Self::parse_number(input_chars)? {
+ Num::Decimal(val) => Ok(BareItem::Decimal(val)),
+ Num::Integer(val) => Ok(BareItem::Integer(val)),
+ },
+ _ => Err("parse_bare_item: item type can't be identified"),
+ }
+ }
+
+ pub(crate) fn parse_bool(input_chars: &mut Peekable<Chars>) -> SFVResult<bool> {
+ // https://httpwg.org/specs/rfc8941.html#parse-boolean
+
+ if input_chars.next() != Some('?') {
+ return Err("parse_bool: first character is not '?'");
+ }
+
+ match input_chars.next() {
+ Some('0') => Ok(false),
+ Some('1') => Ok(true),
+ _ => Err("parse_bool: invalid variant"),
+ }
+ }
+
+ pub(crate) fn parse_string(input_chars: &mut Peekable<Chars>) -> SFVResult<String> {
+ // https://httpwg.org/specs/rfc8941.html#parse-string
+
+ if input_chars.next() != Some('\"') {
+ return Err("parse_string: first character is not '\"'");
+ }
+
+ let mut output_string = String::from("");
+ while let Some(curr_char) = input_chars.next() {
+ match curr_char {
+ '\"' => return Ok(output_string),
+ '\x7f' | '\x00'..='\x1f' => return Err("parse_string: not a visible character"),
+ '\\' => match input_chars.next() {
+ Some(c) if c == '\\' || c == '\"' => {
+ output_string.push(c);
+ }
+ None => return Err("parse_string: last input character is '\\'"),
+ _ => return Err("parse_string: disallowed character after '\\'"),
+ },
+ _ => output_string.push(curr_char),
+ }
+ }
+ Err("parse_string: no closing '\"'")
+ }
+
+ pub(crate) fn parse_token(input_chars: &mut Peekable<Chars>) -> SFVResult<String> {
+ // https://httpwg.org/specs/rfc8941.html#parse-token
+
+ if let Some(first_char) = input_chars.peek() {
+ if !first_char.is_ascii_alphabetic() && first_char != &'*' {
+ return Err("parse_token: first character is not ALPHA or '*'");
+ }
+ } else {
+ return Err("parse_token: empty input string");
+ }
+
+ let mut output_string = String::from("");
+ while let Some(curr_char) = input_chars.peek() {
+ if !utils::is_tchar(*curr_char) && curr_char != &':' && curr_char != &'/' {
+ return Ok(output_string);
+ }
+
+ match input_chars.next() {
+ Some(c) => output_string.push(c),
+ None => return Err("parse_token: end of the string"),
+ }
+ }
+ Ok(output_string)
+ }
+
+ pub(crate) fn parse_byte_sequence(input_chars: &mut Peekable<Chars>) -> SFVResult<Vec<u8>> {
+ // https://httpwg.org/specs/rfc8941.html#parse-binary
+
+ if input_chars.next() != Some(':') {
+ return Err("parse_byte_seq: first char is not ':'");
+ }
+
+ if !input_chars.clone().any(|c| c == ':') {
+ return Err("parse_byte_seq: no closing ':'");
+ }
+
+ let b64_content = input_chars.take_while(|c| c != &':').collect::<String>();
+ if !b64_content.chars().all(utils::is_allowed_b64_content) {
+ return Err("parse_byte_seq: invalid char in byte sequence");
+ }
+ match utils::base64()?.decode(b64_content.as_bytes()) {
+ Ok(content) => Ok(content),
+ Err(_) => Err("parse_byte_seq: decoding error"),
+ }
+ }
+
+ pub(crate) fn parse_number(input_chars: &mut Peekable<Chars>) -> SFVResult<Num> {
+ // https://httpwg.org/specs/rfc8941.html#parse-number
+
+ let mut sign = 1;
+ if let Some('-') = input_chars.peek() {
+ sign = -1;
+ input_chars.next();
+ }
+
+ match input_chars.peek() {
+ Some(c) if !c.is_ascii_digit() => {
+ return Err("parse_number: input number does not start with a digit")
+ }
+ None => return Err("parse_number: input number lacks a digit"),
+ _ => (),
+ }
+
+ // Get number from input as a string and identify whether it's a decimal or integer
+ let (is_integer, input_number) = Self::extract_digits(input_chars)?;
+
+ // Parse input_number from string into integer
+ if is_integer {
+ let output_number = input_number
+ .parse::<i64>()
+ .map_err(|_err| "parse_number: parsing i64 failed")?
+ * sign;
+
+ let (min_int, max_int) = (-999_999_999_999_999_i64, 999_999_999_999_999_i64);
+ if !(min_int <= output_number && output_number <= max_int) {
+ return Err("parse_number: integer number is out of range");
+ }
+
+ return Ok(Num::Integer(output_number));
+ }
+
+ // Parse input_number from string into decimal
+ let chars_after_dot = input_number
+ .find('.')
+ .map(|dot_pos| input_number.len() - dot_pos - 1);
+
+ match chars_after_dot {
+ Some(0) => Err("parse_number: decimal ends with '.'"),
+ Some(1..=3) => {
+ let mut output_number = Decimal::from_str(&input_number)
+ .map_err(|_err| "parse_number: parsing f64 failed")?;
+
+ if sign == -1 {
+ output_number.set_sign_negative(true)
+ }
+
+ Ok(Num::Decimal(output_number))
+ }
+ _ => Err("parse_number: invalid decimal fraction length"),
+ }
+ }
+
+ fn extract_digits(input_chars: &mut Peekable<Chars>) -> SFVResult<(bool, String)> {
+ let mut is_integer = true;
+ let mut input_number = String::from("");
+ while let Some(curr_char) = input_chars.peek() {
+ if curr_char.is_ascii_digit() {
+ input_number.push(*curr_char);
+ input_chars.next();
+ } else if curr_char == &'.' && is_integer {
+ if input_number.len() > 12 {
+ return Err(
+ "parse_number: decimal too long, illegal position for decimal point",
+ );
+ }
+ input_number.push(*curr_char);
+ is_integer = false;
+ input_chars.next();
+ } else {
+ break;
+ }
+
+ if is_integer && input_number.len() > 15 {
+ return Err("parse_number: integer too long, length > 15");
+ }
+
+ if !is_integer && input_number.len() > 16 {
+ return Err("parse_number: decimal too long, length > 16");
+ }
+ }
+ Ok((is_integer, input_number))
+ }
+
+ pub(crate) fn parse_parameters(input_chars: &mut Peekable<Chars>) -> SFVResult<Parameters> {
+ // https://httpwg.org/specs/rfc8941.html#parse-param
+
+ let mut params = Parameters::new();
+
+ while let Some(curr_char) = input_chars.peek() {
+ if curr_char == &';' {
+ input_chars.next();
+ } else {
+ break;
+ }
+
+ utils::consume_sp_chars(input_chars);
+
+ let param_name = Self::parse_key(input_chars)?;
+ let param_value = match input_chars.peek() {
+ Some('=') => {
+ input_chars.next();
+ Self::parse_bare_item(input_chars)?
+ }
+ _ => BareItem::Boolean(true),
+ };
+ params.insert(param_name, param_value);
+ }
+
+ // If parameters already contains a name param_name (comparing character-for-character), overwrite its value.
+ // Note that when duplicate Parameter keys are encountered, this has the effect of ignoring all but the last instance.
+ Ok(params)
+ }
+
+ pub(crate) fn parse_key(input_chars: &mut Peekable<Chars>) -> SFVResult<String> {
+ match input_chars.peek() {
+ Some(c) if c == &'*' || c.is_ascii_lowercase() => (),
+ _ => return Err("parse_key: first character is not lcalpha or '*'"),
+ }
+
+ let mut output = String::new();
+ while let Some(curr_char) = input_chars.peek() {
+ if !curr_char.is_ascii_lowercase()
+ && !curr_char.is_ascii_digit()
+ && !"_-*.".contains(*curr_char)
+ {
+ return Ok(output);
+ }
+
+ output.push(*curr_char);
+ input_chars.next();
+ }
+ Ok(output)
+ }
+}
diff --git a/third_party/rust/sfv/src/ref_serializer.rs b/third_party/rust/sfv/src/ref_serializer.rs
new file mode 100644
index 0000000000..4bcd29ff3a
--- /dev/null
+++ b/third_party/rust/sfv/src/ref_serializer.rs
@@ -0,0 +1,310 @@
+use crate::serializer::Serializer;
+use crate::{RefBareItem, SFVResult};
+use std::marker::PhantomData;
+
+/// Serializes `Item` field value components incrementally.
+/// ```
+/// use sfv::{RefBareItem, RefItemSerializer};
+///
+/// let mut serialized_item = String::new();
+/// let serializer = RefItemSerializer::new(&mut serialized_item);
+/// serializer
+/// .bare_item(&RefBareItem::Integer(11))
+/// .unwrap()
+/// .parameter("foo", &RefBareItem::Boolean(true))
+/// .unwrap();
+/// assert_eq!(serialized_item, "11;foo");
+/// ```
+#[derive(Debug)]
+pub struct RefItemSerializer<'a> {
+ pub buffer: &'a mut String,
+}
+
+impl<'a> RefItemSerializer<'a> {
+ pub fn new(buffer: &'a mut String) -> Self {
+ RefItemSerializer { buffer }
+ }
+
+ pub fn bare_item(self, bare_item: &RefBareItem) -> SFVResult<RefParameterSerializer<'a>> {
+ Serializer::serialize_ref_bare_item(bare_item, self.buffer)?;
+ Ok(RefParameterSerializer {
+ buffer: self.buffer,
+ })
+ }
+}
+
+/// Used by `RefItemSerializer`, `RefListSerializer`, `RefDictSerializer` to serialize a single `Parameter`.
+#[derive(Debug)]
+pub struct RefParameterSerializer<'a> {
+ buffer: &'a mut String,
+}
+
+impl<'a> RefParameterSerializer<'a> {
+ pub fn parameter(self, name: &str, value: &RefBareItem) -> SFVResult<Self> {
+ Serializer::serialize_ref_parameter(name, value, self.buffer)?;
+ Ok(self)
+ }
+}
+
+/// Serializes `List` field value components incrementally.
+/// ```
+/// use sfv::{RefBareItem, RefListSerializer};
+///
+/// let mut serialized_item = String::new();
+/// let serializer = RefListSerializer::new(&mut serialized_item);
+/// serializer
+/// .bare_item(&RefBareItem::Integer(11))
+/// .unwrap()
+/// .parameter("foo", &RefBareItem::Boolean(true))
+/// .unwrap()
+/// .open_inner_list()
+/// .inner_list_bare_item(&RefBareItem::Token("abc"))
+/// .unwrap()
+/// .inner_list_parameter("abc_param", &RefBareItem::Boolean(false))
+/// .unwrap()
+/// .inner_list_bare_item(&RefBareItem::Token("def"))
+/// .unwrap()
+/// .close_inner_list()
+/// .parameter("bar", &RefBareItem::String("val"))
+/// .unwrap();
+/// assert_eq!(
+/// serialized_item,
+/// "11;foo, (abc;abc_param=?0 def);bar=\"val\""
+/// );
+/// ```
+#[derive(Debug)]
+pub struct RefListSerializer<'a> {
+ buffer: &'a mut String,
+}
+
+impl<'a> RefListSerializer<'a> {
+ pub fn new(buffer: &'a mut String) -> Self {
+ RefListSerializer { buffer }
+ }
+
+ pub fn bare_item(self, bare_item: &RefBareItem) -> SFVResult<Self> {
+ if !self.buffer.is_empty() {
+ self.buffer.push_str(", ");
+ }
+ Serializer::serialize_ref_bare_item(bare_item, self.buffer)?;
+ Ok(RefListSerializer {
+ buffer: self.buffer,
+ })
+ }
+
+ pub fn parameter(self, name: &str, value: &RefBareItem) -> SFVResult<Self> {
+ if self.buffer.is_empty() {
+ return Err("parameters must be serialized after bare item or inner list");
+ }
+ Serializer::serialize_ref_parameter(name, value, self.buffer)?;
+ Ok(RefListSerializer {
+ buffer: self.buffer,
+ })
+ }
+ pub fn open_inner_list(self) -> RefInnerListSerializer<'a, Self> {
+ if !self.buffer.is_empty() {
+ self.buffer.push_str(", ");
+ }
+ self.buffer.push('(');
+ RefInnerListSerializer::<RefListSerializer> {
+ buffer: self.buffer,
+ caller_type: PhantomData,
+ }
+ }
+}
+
+/// Serializes `Dictionary` field value components incrementally.
+/// ```
+/// use sfv::{RefBareItem, RefDictSerializer, Decimal, FromPrimitive};
+///
+/// let mut serialized_item = String::new();
+/// let serializer = RefDictSerializer::new(&mut serialized_item);
+/// serializer
+/// .bare_item_member("member1", &RefBareItem::Integer(11))
+/// .unwrap()
+/// .parameter("foo", &RefBareItem::Boolean(true))
+/// .unwrap()
+/// .open_inner_list("member2")
+/// .unwrap()
+/// .inner_list_bare_item(&RefBareItem::Token("abc"))
+/// .unwrap()
+/// .inner_list_parameter("abc_param", &RefBareItem::Boolean(false))
+/// .unwrap()
+/// .inner_list_bare_item(&RefBareItem::Token("def"))
+/// .unwrap()
+/// .close_inner_list()
+/// .parameter("bar", &RefBareItem::String("val"))
+/// .unwrap()
+/// .bare_item_member(
+/// "member3",
+/// &RefBareItem::Decimal(Decimal::from_f64(12.34566).unwrap()),
+/// )
+/// .unwrap();
+/// assert_eq!(
+/// serialized_item,
+/// "member1=11;foo, member2=(abc;abc_param=?0 def);bar=\"val\", member3=12.346"
+/// );
+/// ```
+#[derive(Debug)]
+pub struct RefDictSerializer<'a> {
+ buffer: &'a mut String,
+}
+
+impl<'a> RefDictSerializer<'a> {
+ pub fn new(buffer: &'a mut String) -> Self {
+ RefDictSerializer { buffer }
+ }
+
+ pub fn bare_item_member(self, name: &str, value: &RefBareItem) -> SFVResult<Self> {
+ if !self.buffer.is_empty() {
+ self.buffer.push_str(", ");
+ }
+ Serializer::serialize_key(name, self.buffer)?;
+ if value != &RefBareItem::Boolean(true) {
+ self.buffer.push('=');
+ Serializer::serialize_ref_bare_item(value, self.buffer)?;
+ }
+ Ok(self)
+ }
+
+ pub fn parameter(self, name: &str, value: &RefBareItem) -> SFVResult<Self> {
+ if self.buffer.is_empty() {
+ return Err("parameters must be serialized after bare item or inner list");
+ }
+ Serializer::serialize_ref_parameter(name, value, self.buffer)?;
+ Ok(RefDictSerializer {
+ buffer: self.buffer,
+ })
+ }
+
+ pub fn open_inner_list(self, name: &str) -> SFVResult<RefInnerListSerializer<'a, Self>> {
+ if !self.buffer.is_empty() {
+ self.buffer.push_str(", ");
+ }
+ Serializer::serialize_key(name, self.buffer)?;
+ self.buffer.push_str("=(");
+ Ok(RefInnerListSerializer::<RefDictSerializer> {
+ buffer: self.buffer,
+ caller_type: PhantomData,
+ })
+ }
+}
+
+/// Used by `RefItemSerializer`, `RefListSerializer`, `RefDictSerializer` to serialize `InnerList`.
+#[derive(Debug)]
+pub struct RefInnerListSerializer<'a, T> {
+ buffer: &'a mut String,
+ caller_type: PhantomData<T>,
+}
+
+impl<'a, T: Container<'a>> RefInnerListSerializer<'a, T> {
+ pub fn inner_list_bare_item(self, bare_item: &RefBareItem) -> SFVResult<Self> {
+ if !self.buffer.is_empty() & !self.buffer.ends_with('(') {
+ self.buffer.push(' ');
+ }
+ Serializer::serialize_ref_bare_item(bare_item, self.buffer)?;
+ Ok(RefInnerListSerializer {
+ buffer: self.buffer,
+ caller_type: PhantomData,
+ })
+ }
+
+ pub fn inner_list_parameter(self, name: &str, value: &RefBareItem) -> SFVResult<Self> {
+ if self.buffer.is_empty() {
+ return Err("parameters must be serialized after bare item or inner list");
+ }
+ Serializer::serialize_ref_parameter(name, value, self.buffer)?;
+ Ok(RefInnerListSerializer {
+ buffer: self.buffer,
+ caller_type: PhantomData,
+ })
+ }
+
+ pub fn close_inner_list(self) -> T {
+ self.buffer.push(')');
+ T::new(self.buffer)
+ }
+}
+
+pub trait Container<'a> {
+ fn new(buffer: &'a mut String) -> Self;
+}
+
+impl<'a> Container<'a> for RefListSerializer<'a> {
+ fn new(buffer: &mut String) -> RefListSerializer {
+ RefListSerializer { buffer }
+ }
+}
+
+impl<'a> Container<'a> for RefDictSerializer<'a> {
+ fn new(buffer: &mut String) -> RefDictSerializer {
+ RefDictSerializer { buffer }
+ }
+}
+
+#[cfg(test)]
+mod alternative_serializer_tests {
+ use super::*;
+ use crate::{Decimal, FromPrimitive};
+
+ #[test]
+ fn test_fast_serialize_item() -> SFVResult<()> {
+ let mut output = String::new();
+ let ser = RefItemSerializer::new(&mut output);
+ ser.bare_item(&RefBareItem::Token("hello"))?
+ .parameter("abc", &RefBareItem::Boolean(true))?;
+ assert_eq!("hello;abc", output);
+ Ok(())
+ }
+
+ #[test]
+ fn test_fast_serialize_list() -> SFVResult<()> {
+ let mut output = String::new();
+ let ser = RefListSerializer::new(&mut output);
+ ser.bare_item(&RefBareItem::Token("hello"))?
+ .parameter("key1", &RefBareItem::Boolean(true))?
+ .parameter("key2", &RefBareItem::Boolean(false))?
+ .open_inner_list()
+ .inner_list_bare_item(&RefBareItem::String("some_string"))?
+ .inner_list_bare_item(&RefBareItem::Integer(12))?
+ .inner_list_parameter("inner-member-key", &RefBareItem::Boolean(true))?
+ .close_inner_list()
+ .parameter("inner-list-param", &RefBareItem::Token("*"))?;
+ assert_eq!(
+ "hello;key1;key2=?0, (\"some_string\" 12;inner-member-key);inner-list-param=*",
+ output
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn test_fast_serialize_dict() -> SFVResult<()> {
+ let mut output = String::new();
+ let ser = RefDictSerializer::new(&mut output);
+ ser.bare_item_member("member1", &RefBareItem::Token("hello"))?
+ .parameter("key1", &RefBareItem::Boolean(true))?
+ .parameter("key2", &RefBareItem::Boolean(false))?
+ .bare_item_member("member2", &RefBareItem::Boolean(true))?
+ .parameter(
+ "key3",
+ &RefBareItem::Decimal(Decimal::from_f64(45.4586).unwrap()),
+ )?
+ .parameter("key4", &RefBareItem::String("str"))?
+ .open_inner_list("key5")?
+ .inner_list_bare_item(&RefBareItem::Integer(45))?
+ .inner_list_bare_item(&RefBareItem::Integer(0))?
+ .close_inner_list()
+ .bare_item_member("key6", &RefBareItem::String("foo"))?
+ .open_inner_list("key7")?
+ .inner_list_bare_item(&RefBareItem::ByteSeq("some_string".as_bytes()))?
+ .inner_list_bare_item(&RefBareItem::ByteSeq("other_string".as_bytes()))?
+ .close_inner_list()
+ .parameter("lparam", &RefBareItem::Integer(10))?
+ .bare_item_member("key8", &RefBareItem::Boolean(true))?;
+ assert_eq!(
+ "member1=hello;key1;key2=?0, member2;key3=45.459;key4=\"str\", key5=(45 0), key6=\"foo\", key7=(:c29tZV9zdHJpbmc=: :b3RoZXJfc3RyaW5n:);lparam=10, key8",
+ output
+ );
+ Ok(())
+ }
+}
diff --git a/third_party/rust/sfv/src/serializer.rs b/third_party/rust/sfv/src/serializer.rs
new file mode 100644
index 0000000000..b0ed021766
--- /dev/null
+++ b/third_party/rust/sfv/src/serializer.rs
@@ -0,0 +1,319 @@
+use crate::utils;
+use crate::{
+ BareItem, Decimal, Dictionary, InnerList, Item, List, ListEntry, Parameters, RefBareItem,
+ SFVResult,
+};
+use data_encoding::BASE64;
+
+/// Serializes structured field value into String.
+pub trait SerializeValue {
+ /// Serializes structured field value into String.
+ /// # Examples
+ /// ```
+ /// # use sfv::{Parser, SerializeValue, ParseValue};
+ ///
+ /// let parsed_list_field = Parser::parse_list("\"london\", \t\t\"berlin\"".as_bytes());
+ /// assert!(parsed_list_field.is_ok());
+ ///
+ /// assert_eq!(
+ /// parsed_list_field.unwrap().serialize_value().unwrap(),
+ /// "\"london\", \"berlin\""
+ /// );
+ /// ```
+ fn serialize_value(&self) -> SFVResult<String>;
+}
+
+impl SerializeValue for Dictionary {
+ fn serialize_value(&self) -> SFVResult<String> {
+ let mut output = String::new();
+ Serializer::serialize_dict(self, &mut output)?;
+ Ok(output)
+ }
+}
+
+impl SerializeValue for List {
+ fn serialize_value(&self) -> SFVResult<String> {
+ let mut output = String::new();
+ Serializer::serialize_list(self, &mut output)?;
+ Ok(output)
+ }
+}
+
+impl SerializeValue for Item {
+ fn serialize_value(&self) -> SFVResult<String> {
+ let mut output = String::new();
+ Serializer::serialize_item(self, &mut output)?;
+ Ok(output)
+ }
+}
+
+/// Container serialization functions
+pub(crate) struct Serializer;
+
+impl Serializer {
+ pub(crate) fn serialize_item(input_item: &Item, output: &mut String) -> SFVResult<()> {
+ // https://httpwg.org/specs/rfc8941.html#ser-item
+
+ Self::serialize_bare_item(&input_item.bare_item, output)?;
+ Self::serialize_parameters(&input_item.params, output)?;
+ Ok(())
+ }
+
+ #[allow(clippy::ptr_arg)]
+ pub(crate) fn serialize_list(input_list: &List, output: &mut String) -> SFVResult<()> {
+ // https://httpwg.org/specs/rfc8941.html#ser-list
+ if input_list.is_empty() {
+ return Err("serialize_list: serializing empty field is not allowed");
+ }
+
+ for (idx, member) in input_list.iter().enumerate() {
+ match member {
+ ListEntry::Item(item) => {
+ Self::serialize_item(item, output)?;
+ }
+ ListEntry::InnerList(inner_list) => {
+ Self::serialize_inner_list(inner_list, output)?;
+ }
+ };
+
+ // If more items remain in input_list:
+ // Append “,” to output.
+ // Append a single SP to output.
+ if idx < input_list.len() - 1 {
+ output.push_str(", ");
+ }
+ }
+ Ok(())
+ }
+
+ pub(crate) fn serialize_dict(input_dict: &Dictionary, output: &mut String) -> SFVResult<()> {
+ // https://httpwg.org/specs/rfc8941.html#ser-dictionary
+ if input_dict.is_empty() {
+ return Err("serialize_dictionary: serializing empty field is not allowed");
+ }
+
+ for (idx, (member_name, member_value)) in input_dict.iter().enumerate() {
+ Serializer::serialize_key(member_name, output)?;
+
+ match member_value {
+ ListEntry::Item(ref item) => {
+ // If dict member is boolean true, no need to serialize it: only its params must be serialized
+ // Otherwise serialize entire item with its params
+ if item.bare_item == BareItem::Boolean(true) {
+ Self::serialize_parameters(&item.params, output)?;
+ } else {
+ output.push('=');
+ Self::serialize_item(item, output)?;
+ }
+ }
+ ListEntry::InnerList(inner_list) => {
+ output.push('=');
+ Self::serialize_inner_list(inner_list, output)?;
+ }
+ }
+
+ // If more items remain in input_dictionary:
+ // Append “,” to output.
+ // Append a single SP to output.
+ if idx < input_dict.len() - 1 {
+ output.push_str(", ");
+ }
+ }
+ Ok(())
+ }
+
+ fn serialize_inner_list(input_inner_list: &InnerList, output: &mut String) -> SFVResult<()> {
+ // https://httpwg.org/specs/rfc8941.html#ser-innerlist
+
+ let items = &input_inner_list.items;
+ let inner_list_parameters = &input_inner_list.params;
+
+ output.push('(');
+ for (idx, item) in items.iter().enumerate() {
+ Self::serialize_item(item, output)?;
+
+ // If more values remain in inner_list, append a single SP to output
+ if idx < items.len() - 1 {
+ output.push(' ');
+ }
+ }
+ output.push(')');
+ Self::serialize_parameters(inner_list_parameters, output)?;
+ Ok(())
+ }
+
+ pub(crate) fn serialize_bare_item(
+ input_bare_item: &BareItem,
+ output: &mut String,
+ ) -> SFVResult<()> {
+ // https://httpwg.org/specs/rfc8941.html#ser-bare-item
+
+ let ref_bare_item = input_bare_item.to_ref_bare_item();
+ Self::serialize_ref_bare_item(&ref_bare_item, output)
+ }
+
+ pub(crate) fn serialize_ref_bare_item(
+ value: &RefBareItem,
+ output: &mut String,
+ ) -> SFVResult<()> {
+ match value {
+ RefBareItem::Boolean(value) => Self::serialize_bool(*value, output)?,
+ RefBareItem::String(value) => Self::serialize_string(value, output)?,
+ RefBareItem::ByteSeq(value) => Self::serialize_byte_sequence(value, output)?,
+ RefBareItem::Token(value) => Self::serialize_token(value, output)?,
+ RefBareItem::Integer(value) => Self::serialize_integer(*value, output)?,
+ RefBareItem::Decimal(value) => Self::serialize_decimal(*value, output)?,
+ };
+ Ok(())
+ }
+
+ pub(crate) fn serialize_parameters(
+ input_params: &Parameters,
+ output: &mut String,
+ ) -> SFVResult<()> {
+ // https://httpwg.org/specs/rfc8941.html#ser-params
+
+ for (param_name, param_value) in input_params.iter() {
+ Self::serialize_ref_parameter(param_name, &param_value.to_ref_bare_item(), output)?;
+ }
+ Ok(())
+ }
+
+ pub(crate) fn serialize_ref_parameter(
+ name: &str,
+ value: &RefBareItem,
+ output: &mut String,
+ ) -> SFVResult<()> {
+ output.push(';');
+ Self::serialize_key(name, output)?;
+
+ if value != &RefBareItem::Boolean(true) {
+ output.push('=');
+ Self::serialize_ref_bare_item(value, output)?;
+ }
+ Ok(())
+ }
+
+ pub(crate) fn serialize_key(input_key: &str, output: &mut String) -> SFVResult<()> {
+ // https://httpwg.org/specs/rfc8941.html#ser-key
+
+ let disallowed_chars =
+ |c: char| !(c.is_ascii_lowercase() || c.is_ascii_digit() || "_-*.".contains(c));
+
+ if input_key.chars().any(disallowed_chars) {
+ return Err("serialize_key: disallowed character in input");
+ }
+
+ if let Some(char) = input_key.chars().next() {
+ if !(char.is_ascii_lowercase() || char == '*') {
+ return Err("serialize_key: first character is not lcalpha or '*'");
+ }
+ }
+ output.push_str(input_key);
+ Ok(())
+ }
+
+ pub(crate) fn serialize_integer(value: i64, output: &mut String) -> SFVResult<()> {
+ //https://httpwg.org/specs/rfc8941.html#ser-integer
+
+ let (min_int, max_int) = (-999_999_999_999_999_i64, 999_999_999_999_999_i64);
+ if !(min_int <= value && value <= max_int) {
+ return Err("serialize_integer: integer is out of range");
+ }
+ output.push_str(&value.to_string());
+ Ok(())
+ }
+
+ pub(crate) fn serialize_decimal(value: Decimal, output: &mut String) -> SFVResult<()> {
+ // https://httpwg.org/specs/rfc8941.html#ser-decimal
+
+ let integer_comp_length = 12;
+ let fraction_length = 3;
+
+ let decimal = value.round_dp(fraction_length);
+ let int_comp = decimal.trunc();
+ let fract_comp = decimal.fract();
+
+ // TODO: Replace with > 999_999_999_999_u64
+ if int_comp.abs().to_string().len() > integer_comp_length {
+ return Err("serialize_decimal: integer component > 12 digits");
+ }
+
+ if fract_comp.is_zero() {
+ output.push_str(&int_comp.to_string());
+ output.push('.');
+ output.push('0');
+ } else {
+ output.push_str(&decimal.to_string());
+ }
+
+ Ok(())
+ }
+
+ pub(crate) fn serialize_string(value: &str, output: &mut String) -> SFVResult<()> {
+ // https://httpwg.org/specs/rfc8941.html#ser-integer
+
+ if !value.is_ascii() {
+ return Err("serialize_string: non-ascii character");
+ }
+
+ let vchar_or_sp = |char| char == '\x7f' || ('\x00'..='\x1f').contains(&char);
+ if value.chars().any(vchar_or_sp) {
+ return Err("serialize_string: not a visible character");
+ }
+
+ output.push('\"');
+ for char in value.chars() {
+ if char == '\\' || char == '\"' {
+ output.push('\\');
+ }
+ output.push(char);
+ }
+ output.push('\"');
+
+ Ok(())
+ }
+
+ pub(crate) fn serialize_token(value: &str, output: &mut String) -> SFVResult<()> {
+ // https://httpwg.org/specs/rfc8941.html#ser-token
+
+ if !value.is_ascii() {
+ return Err("serialize_string: non-ascii character");
+ }
+
+ let mut chars = value.chars();
+ if let Some(char) = chars.next() {
+ if !(char.is_ascii_alphabetic() || char == '*') {
+ return Err("serialise_token: first character is not ALPHA or '*'");
+ }
+ }
+
+ if chars
+ .clone()
+ .any(|c| !(utils::is_tchar(c) || c == ':' || c == '/'))
+ {
+ return Err("serialise_token: disallowed character");
+ }
+
+ output.push_str(value);
+ Ok(())
+ }
+
+ pub(crate) fn serialize_byte_sequence(value: &[u8], output: &mut String) -> SFVResult<()> {
+ // https://httpwg.org/specs/rfc8941.html#ser-binary
+
+ output.push(':');
+ let encoded = BASE64.encode(value.as_ref());
+ output.push_str(&encoded);
+ output.push(':');
+ Ok(())
+ }
+
+ pub(crate) fn serialize_bool(value: bool, output: &mut String) -> SFVResult<()> {
+ // https://httpwg.org/specs/rfc8941.html#ser-boolean
+
+ let val = if value { "?1" } else { "?0" };
+ output.push_str(val);
+ Ok(())
+ }
+}
diff --git a/third_party/rust/sfv/src/test_parser.rs b/third_party/rust/sfv/src/test_parser.rs
new file mode 100644
index 0000000000..97404eb951
--- /dev/null
+++ b/third_party/rust/sfv/src/test_parser.rs
@@ -0,0 +1,850 @@
+use crate::FromStr;
+use crate::{BareItem, Decimal, Dictionary, InnerList, Item, List, Num, Parameters};
+use crate::{ParseMore, ParseValue, Parser};
+use std::error::Error;
+use std::iter::FromIterator;
+
+#[test]
+fn parse() -> Result<(), Box<dyn Error>> {
+ let input = "\"some_value\"".as_bytes();
+ let parsed_item = Item::new(BareItem::String("some_value".to_owned()));
+ let expected = parsed_item;
+ assert_eq!(expected, Parser::parse_item(input)?);
+
+ let input = "12.35;a ".as_bytes();
+ let params = Parameters::from_iter(vec![("a".to_owned(), BareItem::Boolean(true))]);
+ let expected = Item::with_params(Decimal::from_str("12.35")?.into(), params);
+
+ assert_eq!(expected, Parser::parse_item(input)?);
+ Ok(())
+}
+
+#[test]
+fn parse_errors() -> Result<(), Box<dyn Error>> {
+ let input = "\"some_value¢\"".as_bytes();
+ assert_eq!(
+ Err("parse: non-ascii characters in input"),
+ Parser::parse_item(input)
+ );
+ let input = "\"some_value\" trailing_text".as_bytes();
+ assert_eq!(
+ Err("parse: trailing characters after parsed value"),
+ Parser::parse_item(input)
+ );
+ assert_eq!(
+ Err("parse_bare_item: empty item"),
+ Parser::parse_item("".as_bytes())
+ );
+ Ok(())
+}
+
+#[test]
+fn parse_list_of_numbers() -> Result<(), Box<dyn Error>> {
+ let mut input = "1,42".chars().peekable();
+ let item1 = Item::new(1.into());
+ let item2 = Item::new(42.into());
+ let expected_list: List = vec![item1.into(), item2.into()];
+ assert_eq!(expected_list, List::parse(&mut input)?);
+ Ok(())
+}
+
+#[test]
+fn parse_list_with_multiple_spaces() -> Result<(), Box<dyn Error>> {
+ let mut input = "1 , 42".chars().peekable();
+ let item1 = Item::new(1.into());
+ let item2 = Item::new(42.into());
+ let expected_list: List = vec![item1.into(), item2.into()];
+ assert_eq!(expected_list, List::parse(&mut input)?);
+ Ok(())
+}
+
+#[test]
+fn parse_list_of_lists() -> Result<(), Box<dyn Error>> {
+ let mut input = "(1 2), (42 43)".chars().peekable();
+ let item1 = Item::new(1.into());
+ let item2 = Item::new(2.into());
+ let item3 = Item::new(42.into());
+ let item4 = Item::new(43.into());
+ let inner_list_1 = InnerList::new(vec![item1, item2]);
+ let inner_list_2 = InnerList::new(vec![item3, item4]);
+ let expected_list: List = vec![inner_list_1.into(), inner_list_2.into()];
+ assert_eq!(expected_list, List::parse(&mut input)?);
+ Ok(())
+}
+
+#[test]
+fn parse_list_empty_inner_list() -> Result<(), Box<dyn Error>> {
+ let mut input = "()".chars().peekable();
+ let inner_list = InnerList::new(vec![]);
+ let expected_list: List = vec![inner_list.into()];
+ assert_eq!(expected_list, List::parse(&mut input)?);
+ Ok(())
+}
+
+#[test]
+fn parse_list_empty() -> Result<(), Box<dyn Error>> {
+ let mut input = "".chars().peekable();
+ let expected_list: List = vec![];
+ assert_eq!(expected_list, List::parse(&mut input)?);
+ Ok(())
+}
+
+#[test]
+fn parse_list_of_lists_with_param_and_spaces() -> Result<(), Box<dyn Error>> {
+ let mut input = "( 1 42 ); k=*".chars().peekable();
+ let item1 = Item::new(1.into());
+ let item2 = Item::new(42.into());
+ let inner_list_param =
+ Parameters::from_iter(vec![("k".to_owned(), BareItem::Token("*".to_owned()))]);
+ let inner_list = InnerList::with_params(vec![item1, item2], inner_list_param);
+ let expected_list: List = vec![inner_list.into()];
+ assert_eq!(expected_list, List::parse(&mut input)?);
+ Ok(())
+}
+
+#[test]
+fn parse_list_of_items_and_lists_with_param() -> Result<(), Box<dyn Error>> {
+ let mut input = "12, 14, (a b); param=\"param_value_1\", ()"
+ .chars()
+ .peekable();
+ let item1 = Item::new(12.into());
+ let item2 = Item::new(14.into());
+ let item3 = Item::new(BareItem::Token("a".to_owned()));
+ let item4 = Item::new(BareItem::Token("b".to_owned()));
+ let inner_list_param = Parameters::from_iter(vec![(
+ "param".to_owned(),
+ BareItem::String("param_value_1".to_owned()),
+ )]);
+ let inner_list = InnerList::with_params(vec![item3, item4], inner_list_param);
+ let empty_inner_list = InnerList::new(vec![]);
+ let expected_list: List = vec![
+ item1.into(),
+ item2.into(),
+ inner_list.into(),
+ empty_inner_list.into(),
+ ];
+ assert_eq!(expected_list, List::parse(&mut input)?);
+ Ok(())
+}
+
+#[test]
+fn parse_list_errors() -> Result<(), Box<dyn Error>> {
+ let mut input = ",".chars().peekable();
+ assert_eq!(
+ Err("parse_bare_item: item type can't be identified"),
+ List::parse(&mut input)
+ );
+
+ let mut input = "a, b c".chars().peekable();
+ assert_eq!(
+ Err("parse_list: trailing characters after list member"),
+ List::parse(&mut input)
+ );
+
+ let mut input = "a,".chars().peekable();
+ assert_eq!(Err("parse_list: trailing comma"), List::parse(&mut input));
+
+ let mut input = "a , ".chars().peekable();
+ assert_eq!(Err("parse_list: trailing comma"), List::parse(&mut input));
+
+ let mut input = "a\t \t ,\t ".chars().peekable();
+ assert_eq!(Err("parse_list: trailing comma"), List::parse(&mut input));
+
+ let mut input = "a\t\t,\t\t\t".chars().peekable();
+ assert_eq!(Err("parse_list: trailing comma"), List::parse(&mut input));
+
+ let mut input = "(a b),".chars().peekable();
+ assert_eq!(Err("parse_list: trailing comma"), List::parse(&mut input));
+
+ let mut input = "(1, 2, (a b)".chars().peekable();
+ assert_eq!(
+ Err("parse_inner_list: bad delimitation"),
+ List::parse(&mut input)
+ );
+
+ Ok(())
+}
+
+#[test]
+fn parse_inner_list_errors() -> Result<(), Box<dyn Error>> {
+ let mut input = "c b); a=1".chars().peekable();
+ assert_eq!(
+ Err("parse_inner_list: input does not start with '('"),
+ Parser::parse_inner_list(&mut input)
+ );
+ Ok(())
+}
+
+#[test]
+fn parse_inner_list_with_param_and_spaces() -> Result<(), Box<dyn Error>> {
+ let mut input = "(c b); a=1".chars().peekable();
+ let inner_list_param = Parameters::from_iter(vec![("a".to_owned(), 1.into())]);
+
+ let item1 = Item::new(BareItem::Token("c".to_owned()));
+ let item2 = Item::new(BareItem::Token("b".to_owned()));
+ let expected = InnerList::with_params(vec![item1, item2], inner_list_param);
+ assert_eq!(expected, Parser::parse_inner_list(&mut input)?);
+ Ok(())
+}
+
+#[test]
+fn parse_item_int_with_space() -> Result<(), Box<dyn Error>> {
+ let mut input = "12 ".chars().peekable();
+ assert_eq!(Item::new(12.into()), Item::parse(&mut input)?);
+ Ok(())
+}
+
+#[test]
+fn parse_item_decimal_with_bool_param_and_space() -> Result<(), Box<dyn Error>> {
+ let mut input = "12.35;a ".chars().peekable();
+ let param = Parameters::from_iter(vec![("a".to_owned(), BareItem::Boolean(true))]);
+ assert_eq!(
+ Item::with_params(Decimal::from_str("12.35")?.into(), param),
+ Item::parse(&mut input)?
+ );
+ Ok(())
+}
+
+#[test]
+fn parse_item_number_with_param() -> Result<(), Box<dyn Error>> {
+ let param = Parameters::from_iter(vec![("a1".to_owned(), BareItem::Token("*".to_owned()))]);
+ assert_eq!(
+ Item::with_params(BareItem::String("12.35".to_owned()), param),
+ Item::parse(&mut "\"12.35\";a1=*".chars().peekable())?
+ );
+ Ok(())
+}
+
+#[test]
+fn parse_item_errors() -> Result<(), Box<dyn Error>> {
+ assert_eq!(
+ Err("parse_bare_item: empty item"),
+ Item::parse(&mut "".chars().peekable())
+ );
+ Ok(())
+}
+
+#[test]
+fn parse_dict_empty() -> Result<(), Box<dyn Error>> {
+ assert_eq!(
+ Dictionary::new(),
+ Dictionary::parse(&mut "".chars().peekable())?
+ );
+ Ok(())
+}
+
+#[test]
+fn parse_dict_errors() -> Result<(), Box<dyn Error>> {
+ let mut input = "abc=123;a=1;b=2 def".chars().peekable();
+ assert_eq!(
+ Err("parse_dict: trailing characters after dictionary member"),
+ Dictionary::parse(&mut input)
+ );
+ let mut input = "abc=123;a=1,".chars().peekable();
+ assert_eq!(
+ Err("parse_dict: trailing comma"),
+ Dictionary::parse(&mut input)
+ );
+ Ok(())
+}
+
+#[test]
+fn parse_dict_with_spaces_and_params() -> Result<(), Box<dyn Error>> {
+ let mut input = "abc=123;a=1;b=2, def=456, ghi=789;q=9;r=\"+w\""
+ .chars()
+ .peekable();
+ let item1_params =
+ Parameters::from_iter(vec![("a".to_owned(), 1.into()), ("b".to_owned(), 2.into())]);
+ let item3_params = Parameters::from_iter(vec![
+ ("q".to_owned(), 9.into()),
+ ("r".to_owned(), BareItem::String("+w".to_owned())),
+ ]);
+
+ let item1 = Item::with_params(123.into(), item1_params);
+ let item2 = Item::new(456.into());
+ let item3 = Item::with_params(789.into(), item3_params);
+
+ let expected_dict = Dictionary::from_iter(vec![
+ ("abc".to_owned(), item1.into()),
+ ("def".to_owned(), item2.into()),
+ ("ghi".to_owned(), item3.into()),
+ ]);
+ assert_eq!(expected_dict, Dictionary::parse(&mut input)?);
+
+ Ok(())
+}
+
+#[test]
+fn parse_dict_empty_value() -> Result<(), Box<dyn Error>> {
+ let mut input = "a=()".chars().peekable();
+ let inner_list = InnerList::new(vec![]);
+ let expected_dict = Dictionary::from_iter(vec![("a".to_owned(), inner_list.into())]);
+ assert_eq!(expected_dict, Dictionary::parse(&mut input)?);
+ Ok(())
+}
+
+#[test]
+fn parse_dict_with_token_param() -> Result<(), Box<dyn Error>> {
+ let mut input = "a=1, b;foo=*, c=3".chars().peekable();
+ let item2_params =
+ Parameters::from_iter(vec![("foo".to_owned(), BareItem::Token("*".to_owned()))]);
+ let item1 = Item::new(1.into());
+ let item2 = Item::with_params(BareItem::Boolean(true), item2_params);
+ let item3 = Item::new(3.into());
+ let expected_dict = Dictionary::from_iter(vec![
+ ("a".to_owned(), item1.into()),
+ ("b".to_owned(), item2.into()),
+ ("c".to_owned(), item3.into()),
+ ]);
+ assert_eq!(expected_dict, Dictionary::parse(&mut input)?);
+ Ok(())
+}
+
+#[test]
+fn parse_dict_multiple_spaces() -> Result<(), Box<dyn Error>> {
+ // input1, input2, input3 must be parsed into the same structure
+ let item1 = Item::new(1.into());
+ let item2 = Item::new(2.into());
+ let expected_dict = Dictionary::from_iter(vec![
+ ("a".to_owned(), item1.into()),
+ ("b".to_owned(), item2.into()),
+ ]);
+
+ let mut input1 = "a=1 , b=2".chars().peekable();
+ let mut input2 = "a=1\t,\tb=2".chars().peekable();
+ let mut input3 = "a=1, b=2".chars().peekable();
+ assert_eq!(expected_dict, Dictionary::parse(&mut input1)?);
+ assert_eq!(expected_dict, Dictionary::parse(&mut input2)?);
+ assert_eq!(expected_dict, Dictionary::parse(&mut input3)?);
+
+ Ok(())
+}
+
+#[test]
+fn parse_bare_item() -> Result<(), Box<dyn Error>> {
+ assert_eq!(
+ BareItem::Boolean(false),
+ Parser::parse_bare_item(&mut "?0".chars().peekable())?
+ );
+ assert_eq!(
+ BareItem::String("test string".to_owned()),
+ Parser::parse_bare_item(&mut "\"test string\"".chars().peekable())?
+ );
+ assert_eq!(
+ BareItem::Token("*token".to_owned()),
+ Parser::parse_bare_item(&mut "*token".chars().peekable())?
+ );
+ assert_eq!(
+ BareItem::ByteSeq("base_64 encoding test".to_owned().into_bytes()),
+ Parser::parse_bare_item(&mut ":YmFzZV82NCBlbmNvZGluZyB0ZXN0:".chars().peekable())?
+ );
+ assert_eq!(
+ BareItem::Decimal(Decimal::from_str("-3.55")?),
+ Parser::parse_bare_item(&mut "-3.55".chars().peekable())?
+ );
+ Ok(())
+}
+
+#[test]
+fn parse_bare_item_errors() -> Result<(), Box<dyn Error>> {
+ assert_eq!(
+ Err("parse_bare_item: item type can't be identified"),
+ Parser::parse_bare_item(&mut "!?0".chars().peekable())
+ );
+ assert_eq!(
+ Err("parse_bare_item: item type can't be identified"),
+ Parser::parse_bare_item(&mut "_11abc".chars().peekable())
+ );
+ assert_eq!(
+ Err("parse_bare_item: item type can't be identified"),
+ Parser::parse_bare_item(&mut " ".chars().peekable())
+ );
+ Ok(())
+}
+
+#[test]
+fn parse_bool() -> Result<(), Box<dyn Error>> {
+ let mut input = "?0gk".chars().peekable();
+ assert_eq!(false, Parser::parse_bool(&mut input)?);
+ assert_eq!(input.collect::<String>(), "gk");
+
+ assert_eq!(false, Parser::parse_bool(&mut "?0".chars().peekable())?);
+ assert_eq!(true, Parser::parse_bool(&mut "?1".chars().peekable())?);
+ Ok(())
+}
+
+#[test]
+fn parse_bool_errors() -> Result<(), Box<dyn Error>> {
+ assert_eq!(
+ Err("parse_bool: first character is not '?'"),
+ Parser::parse_bool(&mut "".chars().peekable())
+ );
+ assert_eq!(
+ Err("parse_bool: invalid variant"),
+ Parser::parse_bool(&mut "?".chars().peekable())
+ );
+ Ok(())
+}
+
+#[test]
+fn parse_string() -> Result<(), Box<dyn Error>> {
+ let mut input = "\"some string\" ;not string".chars().peekable();
+ assert_eq!("some string".to_owned(), Parser::parse_string(&mut input)?);
+ assert_eq!(input.collect::<String>(), " ;not string");
+
+ assert_eq!(
+ "test".to_owned(),
+ Parser::parse_string(&mut "\"test\"".chars().peekable())?
+ );
+ assert_eq!(
+ r#"te\st"#.to_owned(),
+ Parser::parse_string(&mut "\"te\\\\st\"".chars().peekable())?
+ );
+ assert_eq!(
+ "".to_owned(),
+ Parser::parse_string(&mut "\"\"".chars().peekable())?
+ );
+ assert_eq!(
+ "some string".to_owned(),
+ Parser::parse_string(&mut "\"some string\"".chars().peekable())?
+ );
+ Ok(())
+}
+
+#[test]
+fn parse_string_errors() -> Result<(), Box<dyn Error>> {
+ assert_eq!(
+ Err("parse_string: first character is not '\"'"),
+ Parser::parse_string(&mut "test".chars().peekable())
+ );
+ assert_eq!(
+ Err("parse_string: last input character is '\\'"),
+ Parser::parse_string(&mut "\"\\".chars().peekable())
+ );
+ assert_eq!(
+ Err("parse_string: disallowed character after '\\'"),
+ Parser::parse_string(&mut "\"\\l\"".chars().peekable())
+ );
+ assert_eq!(
+ Err("parse_string: not a visible character"),
+ Parser::parse_string(&mut "\"\u{1f}\"".chars().peekable())
+ );
+ assert_eq!(
+ Err("parse_string: no closing '\"'"),
+ Parser::parse_string(&mut "\"smth".chars().peekable())
+ );
+ Ok(())
+}
+
+#[test]
+fn parse_token() -> Result<(), Box<dyn Error>> {
+ let mut input = "*some:token}not token".chars().peekable();
+ assert_eq!("*some:token".to_owned(), Parser::parse_token(&mut input)?);
+ assert_eq!(input.collect::<String>(), "}not token");
+
+ assert_eq!(
+ "token".to_owned(),
+ Parser::parse_token(&mut "token".chars().peekable())?
+ );
+ assert_eq!(
+ "a_b-c.d3:f%00/*".to_owned(),
+ Parser::parse_token(&mut "a_b-c.d3:f%00/*".chars().peekable())?
+ );
+ assert_eq!(
+ "TestToken".to_owned(),
+ Parser::parse_token(&mut "TestToken".chars().peekable())?
+ );
+ assert_eq!(
+ "some".to_owned(),
+ Parser::parse_token(&mut "some@token".chars().peekable())?
+ );
+ assert_eq!(
+ "*TestToken*".to_owned(),
+ Parser::parse_token(&mut "*TestToken*".chars().peekable())?
+ );
+ assert_eq!(
+ "*".to_owned(),
+ Parser::parse_token(&mut "*[@:token".chars().peekable())?
+ );
+ assert_eq!(
+ "test".to_owned(),
+ Parser::parse_token(&mut "test token".chars().peekable())?
+ );
+
+ Ok(())
+}
+
+#[test]
+fn parse_token_errors() -> Result<(), Box<dyn Error>> {
+ let mut input = "765token".chars().peekable();
+ assert_eq!(
+ Err("parse_token: first character is not ALPHA or '*'"),
+ Parser::parse_token(&mut input)
+ );
+ assert_eq!(input.collect::<String>(), "765token");
+
+ assert_eq!(
+ Err("parse_token: first character is not ALPHA or '*'"),
+ Parser::parse_token(&mut "7token".chars().peekable())
+ );
+ assert_eq!(
+ Err("parse_token: empty input string"),
+ Parser::parse_token(&mut "".chars().peekable())
+ );
+ Ok(())
+}
+
+#[test]
+fn parse_byte_sequence() -> Result<(), Box<dyn Error>> {
+ let mut input = ":aGVsbG8:rest_of_str".chars().peekable();
+ assert_eq!(
+ "hello".to_owned().into_bytes(),
+ Parser::parse_byte_sequence(&mut input)?
+ );
+ assert_eq!("rest_of_str", input.collect::<String>());
+
+ assert_eq!(
+ "hello".to_owned().into_bytes(),
+ Parser::parse_byte_sequence(&mut ":aGVsbG8:".chars().peekable())?
+ );
+ assert_eq!(
+ "test_encode".to_owned().into_bytes(),
+ Parser::parse_byte_sequence(&mut ":dGVzdF9lbmNvZGU:".chars().peekable())?
+ );
+ assert_eq!(
+ "new:year tree".to_owned().into_bytes(),
+ Parser::parse_byte_sequence(&mut ":bmV3OnllYXIgdHJlZQ==:".chars().peekable())?
+ );
+ assert_eq!(
+ "".to_owned().into_bytes(),
+ Parser::parse_byte_sequence(&mut "::".chars().peekable())?
+ );
+ Ok(())
+}
+
+#[test]
+fn parse_byte_sequence_errors() -> Result<(), Box<dyn Error>> {
+ assert_eq!(
+ Err("parse_byte_seq: first char is not ':'"),
+ Parser::parse_byte_sequence(&mut "aGVsbG8".chars().peekable())
+ );
+ assert_eq!(
+ Err("parse_byte_seq: invalid char in byte sequence"),
+ Parser::parse_byte_sequence(&mut ":aGVsb G8=:".chars().peekable())
+ );
+ assert_eq!(
+ Err("parse_byte_seq: no closing ':'"),
+ Parser::parse_byte_sequence(&mut ":aGVsbG8=".chars().peekable())
+ );
+ Ok(())
+}
+
+#[test]
+fn parse_number_int() -> Result<(), Box<dyn Error>> {
+ let mut input = "-733333333332d.14".chars().peekable();
+ assert_eq!(
+ Num::Integer(-733333333332),
+ Parser::parse_number(&mut input)?
+ );
+ assert_eq!("d.14", input.collect::<String>());
+
+ assert_eq!(
+ Num::Integer(42),
+ Parser::parse_number(&mut "42".chars().peekable())?
+ );
+ assert_eq!(
+ Num::Integer(-42),
+ Parser::parse_number(&mut "-42".chars().peekable())?
+ );
+ assert_eq!(
+ Num::Integer(-42),
+ Parser::parse_number(&mut "-042".chars().peekable())?
+ );
+ assert_eq!(
+ Num::Integer(0),
+ Parser::parse_number(&mut "0".chars().peekable())?
+ );
+ assert_eq!(
+ Num::Integer(0),
+ Parser::parse_number(&mut "00".chars().peekable())?
+ );
+ assert_eq!(
+ Num::Integer(123456789012345),
+ Parser::parse_number(&mut "123456789012345".chars().peekable())?
+ );
+ assert_eq!(
+ Num::Integer(-123456789012345),
+ Parser::parse_number(&mut "-123456789012345".chars().peekable())?
+ );
+ assert_eq!(
+ Num::Integer(2),
+ Parser::parse_number(&mut "2,3".chars().peekable())?
+ );
+ assert_eq!(
+ Num::Integer(4),
+ Parser::parse_number(&mut "4-2".chars().peekable())?
+ );
+ assert_eq!(
+ Num::Integer(-999999999999999),
+ Parser::parse_number(&mut "-999999999999999".chars().peekable())?
+ );
+ assert_eq!(
+ Num::Integer(999999999999999),
+ Parser::parse_number(&mut "999999999999999".chars().peekable())?
+ );
+
+ Ok(())
+}
+
+#[test]
+fn parse_number_decimal() -> Result<(), Box<dyn Error>> {
+ let mut input = "00.42 test string".chars().peekable();
+ assert_eq!(
+ Num::Decimal(Decimal::from_str("0.42")?),
+ Parser::parse_number(&mut input)?
+ );
+ assert_eq!(" test string", input.collect::<String>());
+
+ assert_eq!(
+ Num::Decimal(Decimal::from_str("1.5")?),
+ Parser::parse_number(&mut "1.5.4.".chars().peekable())?
+ );
+ assert_eq!(
+ Num::Decimal(Decimal::from_str("1.8")?),
+ Parser::parse_number(&mut "1.8.".chars().peekable())?
+ );
+ assert_eq!(
+ Num::Decimal(Decimal::from_str("1.7")?),
+ Parser::parse_number(&mut "1.7.0".chars().peekable())?
+ );
+ assert_eq!(
+ Num::Decimal(Decimal::from_str("3.14")?),
+ Parser::parse_number(&mut "3.14".chars().peekable())?
+ );
+ assert_eq!(
+ Num::Decimal(Decimal::from_str("-3.14")?),
+ Parser::parse_number(&mut "-3.14".chars().peekable())?
+ );
+ assert_eq!(
+ Num::Decimal(Decimal::from_str("123456789012.1")?),
+ Parser::parse_number(&mut "123456789012.1".chars().peekable())?
+ );
+ assert_eq!(
+ Num::Decimal(Decimal::from_str("1234567890.112")?),
+ Parser::parse_number(&mut "1234567890.112".chars().peekable())?
+ );
+
+ Ok(())
+}
+
+#[test]
+fn parse_number_errors() -> Result<(), Box<dyn Error>> {
+ let mut input = ":aGVsbG8:rest".chars().peekable();
+ assert_eq!(
+ Err("parse_number: input number does not start with a digit"),
+ Parser::parse_number(&mut input)
+ );
+ assert_eq!(":aGVsbG8:rest", input.collect::<String>());
+
+ let mut input = "-11.5555 test string".chars().peekable();
+ assert_eq!(
+ Err("parse_number: invalid decimal fraction length"),
+ Parser::parse_number(&mut input)
+ );
+ assert_eq!(" test string", input.collect::<String>());
+
+ assert_eq!(
+ Err("parse_number: input number does not start with a digit"),
+ Parser::parse_number(&mut "--0".chars().peekable())
+ );
+ assert_eq!(
+ Err("parse_number: decimal too long, illegal position for decimal point"),
+ Parser::parse_number(&mut "1999999999999.1".chars().peekable())
+ );
+ assert_eq!(
+ Err("parse_number: decimal ends with '.'"),
+ Parser::parse_number(&mut "19888899999.".chars().peekable())
+ );
+ assert_eq!(
+ Err("parse_number: integer too long, length > 15"),
+ Parser::parse_number(&mut "1999999999999999".chars().peekable())
+ );
+ assert_eq!(
+ Err("parse_number: decimal too long, length > 16"),
+ Parser::parse_number(&mut "19999999999.99991".chars().peekable())
+ );
+ assert_eq!(
+ Err("parse_number: input number does not start with a digit"),
+ Parser::parse_number(&mut "- 42".chars().peekable())
+ );
+ assert_eq!(
+ Err("parse_number: input number does not start with a digit"),
+ Parser::parse_number(&mut "- 42".chars().peekable())
+ );
+ assert_eq!(
+ Err("parse_number: decimal ends with '.'"),
+ Parser::parse_number(&mut "1..4".chars().peekable())
+ );
+ assert_eq!(
+ Err("parse_number: input number lacks a digit"),
+ Parser::parse_number(&mut "-".chars().peekable())
+ );
+ assert_eq!(
+ Err("parse_number: decimal ends with '.'"),
+ Parser::parse_number(&mut "-5. 14".chars().peekable())
+ );
+ assert_eq!(
+ Err("parse_number: decimal ends with '.'"),
+ Parser::parse_number(&mut "7. 1".chars().peekable())
+ );
+ assert_eq!(
+ Err("parse_number: invalid decimal fraction length"),
+ Parser::parse_number(&mut "-7.3333333333".chars().peekable())
+ );
+ assert_eq!(
+ Err("parse_number: decimal too long, illegal position for decimal point"),
+ Parser::parse_number(&mut "-7333333333323.12".chars().peekable())
+ );
+
+ Ok(())
+}
+
+#[test]
+fn parse_params_string() -> Result<(), Box<dyn Error>> {
+ let mut input = ";b=\"param_val\"".chars().peekable();
+ let expected = Parameters::from_iter(vec![(
+ "b".to_owned(),
+ BareItem::String("param_val".to_owned()),
+ )]);
+ assert_eq!(expected, Parser::parse_parameters(&mut input)?);
+ Ok(())
+}
+
+#[test]
+fn parse_params_bool() -> Result<(), Box<dyn Error>> {
+ let mut input = ";b;a".chars().peekable();
+ let expected = Parameters::from_iter(vec![
+ ("b".to_owned(), BareItem::Boolean(true)),
+ ("a".to_owned(), BareItem::Boolean(true)),
+ ]);
+ assert_eq!(expected, Parser::parse_parameters(&mut input)?);
+ Ok(())
+}
+
+#[test]
+fn parse_params_mixed_types() -> Result<(), Box<dyn Error>> {
+ let mut input = ";key1=?0;key2=746.15".chars().peekable();
+ let expected = Parameters::from_iter(vec![
+ ("key1".to_owned(), BareItem::Boolean(false)),
+ ("key2".to_owned(), Decimal::from_str("746.15")?.into()),
+ ]);
+ assert_eq!(expected, Parser::parse_parameters(&mut input)?);
+ Ok(())
+}
+
+#[test]
+fn parse_params_with_spaces() -> Result<(), Box<dyn Error>> {
+ let mut input = "; key1=?0; key2=11111".chars().peekable();
+ let expected = Parameters::from_iter(vec![
+ ("key1".to_owned(), BareItem::Boolean(false)),
+ ("key2".to_owned(), 11111.into()),
+ ]);
+ assert_eq!(expected, Parser::parse_parameters(&mut input)?);
+ Ok(())
+}
+
+#[test]
+fn parse_params_empty() -> Result<(), Box<dyn Error>> {
+ assert_eq!(
+ Parameters::new(),
+ Parser::parse_parameters(&mut " key1=?0; key2=11111".chars().peekable())?
+ );
+ assert_eq!(
+ Parameters::new(),
+ Parser::parse_parameters(&mut "".chars().peekable())?
+ );
+ assert_eq!(
+ Parameters::new(),
+ Parser::parse_parameters(&mut "[;a=1".chars().peekable())?
+ );
+ assert_eq!(
+ Parameters::new(),
+ Parser::parse_parameters(&mut String::new().chars().peekable())?
+ );
+ Ok(())
+}
+
+#[test]
+fn parse_key() -> Result<(), Box<dyn Error>> {
+ assert_eq!(
+ "a".to_owned(),
+ Parser::parse_key(&mut "a=1".chars().peekable())?
+ );
+ assert_eq!(
+ "a1".to_owned(),
+ Parser::parse_key(&mut "a1=10".chars().peekable())?
+ );
+ assert_eq!(
+ "*1".to_owned(),
+ Parser::parse_key(&mut "*1=10".chars().peekable())?
+ );
+ assert_eq!(
+ "f".to_owned(),
+ Parser::parse_key(&mut "f[f=10".chars().peekable())?
+ );
+ Ok(())
+}
+
+#[test]
+fn parse_key_errors() -> Result<(), Box<dyn Error>> {
+ assert_eq!(
+ Err("parse_key: first character is not lcalpha or '*'"),
+ Parser::parse_key(&mut "[*f=10".chars().peekable())
+ );
+ Ok(())
+}
+
+#[test]
+fn parse_more_list() -> Result<(), Box<dyn Error>> {
+ let item1 = Item::new(1.into());
+ let item2 = Item::new(2.into());
+ let item3 = Item::new(42.into());
+ let inner_list_1 = InnerList::new(vec![item1, item2]);
+ let expected_list: List = vec![inner_list_1.into(), item3.into()];
+
+ let mut parsed_header = Parser::parse_list("(1 2)".as_bytes())?;
+ let _ = parsed_header.parse_more("42".as_bytes())?;
+ assert_eq!(expected_list, parsed_header);
+ Ok(())
+}
+
+#[test]
+fn parse_more_dict() -> Result<(), Box<dyn Error>> {
+ let item2_params =
+ Parameters::from_iter(vec![("foo".to_owned(), BareItem::Token("*".to_owned()))]);
+ let item1 = Item::new(1.into());
+ let item2 = Item::with_params(BareItem::Boolean(true), item2_params);
+ let item3 = Item::new(3.into());
+ let expected_dict = Dictionary::from_iter(vec![
+ ("a".to_owned(), item1.into()),
+ ("b".to_owned(), item2.into()),
+ ("c".to_owned(), item3.into()),
+ ]);
+
+ let mut parsed_header = Parser::parse_dictionary("a=1, b;foo=*\t\t".as_bytes())?;
+ let _ = parsed_header.parse_more(" c=3".as_bytes())?;
+ assert_eq!(expected_dict, parsed_header);
+ Ok(())
+}
+
+#[test]
+fn parse_more_errors() -> Result<(), Box<dyn Error>> {
+ let parsed_dict_header =
+ Parser::parse_dictionary("a=1, b;foo=*".as_bytes())?.parse_more(",a".as_bytes());
+ assert!(parsed_dict_header.is_err());
+
+ let parsed_list_header =
+ Parser::parse_list("a, b;foo=*".as_bytes())?.parse_more("(a, 2)".as_bytes());
+ assert!(parsed_list_header.is_err());
+ Ok(())
+}
diff --git a/third_party/rust/sfv/src/test_serializer.rs b/third_party/rust/sfv/src/test_serializer.rs
new file mode 100644
index 0000000000..edcc79ce2a
--- /dev/null
+++ b/third_party/rust/sfv/src/test_serializer.rs
@@ -0,0 +1,531 @@
+use crate::serializer::Serializer;
+use crate::FromStr;
+use crate::SerializeValue;
+use crate::{BareItem, Decimal, Dictionary, InnerList, Item, List, Parameters};
+use std::error::Error;
+use std::iter::FromIterator;
+
+#[test]
+fn serialize_value_empty_dict() -> Result<(), Box<dyn Error>> {
+ let dict_field_value = Dictionary::new();
+ assert_eq!(
+ Err("serialize_dictionary: serializing empty field is not allowed"),
+ dict_field_value.serialize_value()
+ );
+ Ok(())
+}
+
+#[test]
+fn serialize_value_empty_list() -> Result<(), Box<dyn Error>> {
+ let list_field_value = List::new();
+ assert_eq!(
+ Err("serialize_list: serializing empty field is not allowed"),
+ list_field_value.serialize_value()
+ );
+ Ok(())
+}
+
+#[test]
+fn serialize_value_list_mixed_members_with_params() -> Result<(), Box<dyn Error>> {
+ let item1 = Item::new(Decimal::from_str("42.4568")?.into());
+ let item2_param = Parameters::from_iter(vec![("itm2_p".to_owned(), BareItem::Boolean(true))]);
+ let item2 = Item::with_params(17.into(), item2_param);
+
+ let inner_list_item1_param =
+ Parameters::from_iter(vec![("in1_p".to_owned(), BareItem::Boolean(false))]);
+ let inner_list_item1 =
+ Item::with_params(BareItem::String("str1".to_owned()), inner_list_item1_param);
+ let inner_list_item2_param = Parameters::from_iter(vec![(
+ "in2_p".to_owned(),
+ BareItem::String("valu\\e".to_owned()),
+ )]);
+ let inner_list_item2 =
+ Item::with_params(BareItem::Token("str2".to_owned()), inner_list_item2_param);
+ let inner_list_param = Parameters::from_iter(vec![(
+ "inner_list_param".to_owned(),
+ BareItem::ByteSeq("weather".as_bytes().to_vec()),
+ )]);
+ let inner_list =
+ InnerList::with_params(vec![inner_list_item1, inner_list_item2], inner_list_param);
+
+ let list_field_value: List = vec![item1.into(), item2.into(), inner_list.into()];
+ let expected = "42.457, 17;itm2_p, (\"str1\";in1_p=?0 str2;in2_p=\"valu\\\\e\");inner_list_param=:d2VhdGhlcg==:";
+ assert_eq!(expected, list_field_value.serialize_value()?);
+ Ok(())
+}
+
+#[test]
+fn serialize_value_errors() -> Result<(), Box<dyn Error>> {
+ let disallowed_item = Item::new(BareItem::String("non-ascii text 🐹".into()));
+ assert_eq!(
+ Err("serialize_string: non-ascii character"),
+ disallowed_item.serialize_value()
+ );
+
+ let disallowed_item = Item::new(Decimal::from_str("12345678912345.123")?.into());
+ assert_eq!(
+ Err("serialize_decimal: integer component > 12 digits"),
+ disallowed_item.serialize_value()
+ );
+
+ let param_with_disallowed_key = Parameters::from_iter(vec![("_key".to_owned(), 13.into())]);
+ let disallowed_item = Item::with_params(12.into(), param_with_disallowed_key);
+ assert_eq!(
+ Err("serialize_key: first character is not lcalpha or '*'"),
+ disallowed_item.serialize_value()
+ );
+ Ok(())
+}
+
+#[test]
+fn serialize_item_byteseq_with_param() -> Result<(), Box<dyn Error>> {
+ let mut buf = String::new();
+
+ let item_param = ("a".to_owned(), BareItem::Token("*ab_1".into()));
+ let item_param = Parameters::from_iter(vec![item_param]);
+ let item = Item::with_params(BareItem::ByteSeq("parser".as_bytes().to_vec()), item_param);
+ Serializer::serialize_item(&item, &mut buf)?;
+ assert_eq!(":cGFyc2Vy:;a=*ab_1", &buf);
+ Ok(())
+}
+
+#[test]
+fn serialize_item_without_params() -> Result<(), Box<dyn Error>> {
+ let mut buf = String::new();
+ let item = Item::new(1.into());
+ Serializer::serialize_item(&item, &mut buf)?;
+ assert_eq!("1", &buf);
+ Ok(())
+}
+
+#[test]
+fn serialize_item_with_bool_true_param() -> Result<(), Box<dyn Error>> {
+ let mut buf = String::new();
+ let param = Parameters::from_iter(vec![("a".to_owned(), BareItem::Boolean(true))]);
+ let item = Item::with_params(Decimal::from_str("12.35")?.into(), param);
+ Serializer::serialize_item(&item, &mut buf)?;
+ assert_eq!("12.35;a", &buf);
+ Ok(())
+}
+
+#[test]
+fn serialize_item_with_token_param() -> Result<(), Box<dyn Error>> {
+ let mut buf = String::new();
+ let param = Parameters::from_iter(vec![("a1".to_owned(), BareItem::Token("*tok".to_owned()))]);
+ let item = Item::with_params(BareItem::String("12.35".to_owned()), param);
+ Serializer::serialize_item(&item, &mut buf)?;
+ assert_eq!("\"12.35\";a1=*tok", &buf);
+ Ok(())
+}
+
+#[test]
+fn serialize_integer() -> Result<(), Box<dyn Error>> {
+ let mut buf = String::new();
+ Serializer::serialize_integer(-12, &mut buf)?;
+ assert_eq!("-12", &buf);
+
+ buf.clear();
+ Serializer::serialize_integer(0, &mut buf)?;
+ assert_eq!("0", &buf);
+
+ buf.clear();
+ Serializer::serialize_integer(999_999_999_999_999, &mut buf)?;
+ assert_eq!("999999999999999", &buf);
+
+ buf.clear();
+ Serializer::serialize_integer(-999_999_999_999_999, &mut buf)?;
+ assert_eq!("-999999999999999", &buf);
+ Ok(())
+}
+
+#[test]
+fn serialize_integer_errors() -> Result<(), Box<dyn Error>> {
+ let mut buf = String::new();
+ assert_eq!(
+ Err("serialize_integer: integer is out of range"),
+ Serializer::serialize_integer(1_000_000_000_000_000, &mut buf)
+ );
+
+ buf.clear();
+ assert_eq!(
+ Err("serialize_integer: integer is out of range"),
+ Serializer::serialize_integer(-1_000_000_000_000_000, &mut buf)
+ );
+ Ok(())
+}
+
+#[test]
+fn serialize_decimal() -> Result<(), Box<dyn Error>> {
+ let mut buf = String::new();
+ Serializer::serialize_decimal(Decimal::from_str("-99.1346897")?, &mut buf)?;
+ assert_eq!("-99.135", &buf);
+
+ buf.clear();
+ Serializer::serialize_decimal(Decimal::from_str("-1.00")?, &mut buf)?;
+ assert_eq!("-1.0", &buf);
+
+ buf.clear();
+ Serializer::serialize_decimal(
+ Decimal::from_str("-00000000000000000000000099.1346897")?,
+ &mut buf,
+ )?;
+ assert_eq!("-99.135", &buf);
+
+ buf.clear();
+ Serializer::serialize_decimal(Decimal::from_str("100.13")?, &mut buf)?;
+ assert_eq!("100.13", &buf);
+
+ buf.clear();
+ Serializer::serialize_decimal(Decimal::from_str("-100.130")?, &mut buf)?;
+ assert_eq!("-100.130", &buf);
+
+ buf.clear();
+ Serializer::serialize_decimal(Decimal::from_str("-137.0")?, &mut buf)?;
+ assert_eq!("-137.0", &buf);
+
+ buf.clear();
+ Serializer::serialize_decimal(Decimal::from_str("137121212112.123")?, &mut buf)?;
+ assert_eq!("137121212112.123", &buf);
+
+ buf.clear();
+ Serializer::serialize_decimal(Decimal::from_str("137121212112.1238")?, &mut buf)?;
+ assert_eq!("137121212112.124", &buf);
+ Ok(())
+}
+
+#[test]
+fn serialize_decimal_errors() -> Result<(), Box<dyn Error>> {
+ let mut buf = String::new();
+ assert_eq!(
+ Err("serialize_decimal: integer component > 12 digits"),
+ Serializer::serialize_decimal(Decimal::from_str("1371212121121.1")?, &mut buf)
+ );
+ Ok(())
+}
+
+#[test]
+fn serialize_string() -> Result<(), Box<dyn Error>> {
+ let mut buf = String::new();
+ Serializer::serialize_string("1.1 text", &mut buf)?;
+ assert_eq!("\"1.1 text\"", &buf);
+
+ buf.clear();
+ Serializer::serialize_string("hello \"name\"", &mut buf)?;
+ assert_eq!("\"hello \\\"name\\\"\"", &buf);
+
+ buf.clear();
+ Serializer::serialize_string("something\\nothing", &mut buf)?;
+ assert_eq!("\"something\\\\nothing\"", &buf);
+
+ buf.clear();
+ Serializer::serialize_string("", &mut buf)?;
+ assert_eq!("\"\"", &buf);
+
+ buf.clear();
+ Serializer::serialize_string(" ", &mut buf)?;
+ assert_eq!("\" \"", &buf);
+
+ buf.clear();
+ Serializer::serialize_string(" ", &mut buf)?;
+ assert_eq!("\" \"", &buf);
+ Ok(())
+}
+
+#[test]
+fn serialize_string_errors() -> Result<(), Box<dyn Error>> {
+ let mut buf = String::new();
+
+ assert_eq!(
+ Err("serialize_string: not a visible character"),
+ Serializer::serialize_string("text \x00", &mut buf)
+ );
+
+ assert_eq!(
+ Err("serialize_string: not a visible character"),
+ Serializer::serialize_string("text \x1f", &mut buf)
+ );
+ assert_eq!(
+ Err("serialize_string: not a visible character"),
+ Serializer::serialize_string("text \x7f", &mut buf)
+ );
+ assert_eq!(
+ Err("serialize_string: non-ascii character"),
+ Serializer::serialize_string("рядок", &mut buf)
+ );
+ Ok(())
+}
+
+#[test]
+fn serialize_token() -> Result<(), Box<dyn Error>> {
+ let mut buf = String::new();
+ Serializer::serialize_token("*", &mut buf)?;
+ assert_eq!("*", &buf);
+
+ buf.clear();
+ Serializer::serialize_token("abc", &mut buf)?;
+ assert_eq!("abc", &buf);
+
+ buf.clear();
+ Serializer::serialize_token("abc:de", &mut buf)?;
+ assert_eq!("abc:de", &buf);
+
+ buf.clear();
+ Serializer::serialize_token("smth/#!else", &mut buf)?;
+ assert_eq!("smth/#!else", &buf);
+ Ok(())
+}
+
+#[test]
+fn serialize_token_errors() -> Result<(), Box<dyn Error>> {
+ let mut buf = String::new();
+
+ assert_eq!(
+ Err("serialise_token: first character is not ALPHA or '*'"),
+ Serializer::serialize_token("#some", &mut buf)
+ );
+ assert_eq!(
+ Err("serialise_token: disallowed character"),
+ Serializer::serialize_token("s ", &mut buf)
+ );
+ assert_eq!(
+ Err("serialise_token: disallowed character"),
+ Serializer::serialize_token("abc:de\t", &mut buf)
+ );
+ Ok(())
+}
+
+#[test]
+fn serialize_byte_sequence() -> Result<(), Box<dyn Error>> {
+ let mut buf = String::new();
+ Serializer::serialize_byte_sequence("hello".as_bytes(), &mut buf)?;
+ assert_eq!(":aGVsbG8=:", &buf);
+
+ buf.clear();
+ Serializer::serialize_byte_sequence("test_encode".as_bytes(), &mut buf)?;
+ assert_eq!(":dGVzdF9lbmNvZGU=:", &buf);
+
+ buf.clear();
+ Serializer::serialize_byte_sequence("".as_bytes(), &mut buf)?;
+ assert_eq!("::", &buf);
+
+ buf.clear();
+ Serializer::serialize_byte_sequence("pleasure.".as_bytes(), &mut buf)?;
+ assert_eq!(":cGxlYXN1cmUu:", &buf);
+
+ buf.clear();
+ Serializer::serialize_byte_sequence("leasure.".as_bytes(), &mut buf)?;
+ assert_eq!(":bGVhc3VyZS4=:", &buf);
+
+ buf.clear();
+ Serializer::serialize_byte_sequence("easure.".as_bytes(), &mut buf)?;
+ assert_eq!(":ZWFzdXJlLg==:", &buf);
+
+ buf.clear();
+ Serializer::serialize_byte_sequence("asure.".as_bytes(), &mut buf)?;
+ assert_eq!(":YXN1cmUu:", &buf);
+
+ buf.clear();
+ Serializer::serialize_byte_sequence("sure.".as_bytes(), &mut buf)?;
+ assert_eq!(":c3VyZS4=:", &buf);
+
+ Ok(())
+}
+
+#[test]
+fn serialize_bool() -> Result<(), Box<dyn Error>> {
+ let mut buf = String::new();
+ Serializer::serialize_bool(true, &mut buf)?;
+ assert_eq!("?1", &buf);
+
+ buf.clear();
+ Serializer::serialize_bool(false, &mut buf)?;
+ assert_eq!("?0", &buf);
+ Ok(())
+}
+
+#[test]
+fn serialize_params_bool() -> Result<(), Box<dyn Error>> {
+ let mut buf = String::new();
+
+ let input = Parameters::from_iter(vec![
+ ("*b".to_owned(), BareItem::Boolean(true)),
+ ("a.a".to_owned(), BareItem::Boolean(true)),
+ ]);
+
+ Serializer::serialize_parameters(&input, &mut buf)?;
+ assert_eq!(";*b;a.a", &buf);
+ Ok(())
+}
+
+#[test]
+fn serialize_params_string() -> Result<(), Box<dyn Error>> {
+ let mut buf = String::new();
+
+ let input = Parameters::from_iter(vec![(
+ "b".to_owned(),
+ BareItem::String("param_val".to_owned()),
+ )]);
+ Serializer::serialize_parameters(&input, &mut buf)?;
+ assert_eq!(";b=\"param_val\"", &buf);
+ Ok(())
+}
+
+#[test]
+fn serialize_params_numbers() -> Result<(), Box<dyn Error>> {
+ let mut buf = String::new();
+
+ let input = Parameters::from_iter(vec![
+ ("key1".to_owned(), Decimal::from_str("746.15")?.into()),
+ ("key2".to_owned(), 11111.into()),
+ ]);
+ Serializer::serialize_parameters(&input, &mut buf)?;
+ assert_eq!(";key1=746.15;key2=11111", &buf);
+ Ok(())
+}
+
+#[test]
+fn serialize_params_mixed_types() -> Result<(), Box<dyn Error>> {
+ let mut buf = String::new();
+
+ let input = Parameters::from_iter(vec![
+ ("key1".to_owned(), BareItem::Boolean(false)),
+ ("key2".to_owned(), Decimal::from_str("1354.091878")?.into()),
+ ]);
+ Serializer::serialize_parameters(&input, &mut buf)?;
+ assert_eq!(";key1=?0;key2=1354.092", &buf);
+ Ok(())
+}
+
+#[test]
+fn serialize_key() -> Result<(), Box<dyn Error>> {
+ let mut buf = String::new();
+ Serializer::serialize_key("*a_fg", &mut buf)?;
+ assert_eq!("*a_fg", &buf);
+
+ buf.clear();
+ Serializer::serialize_key("*a_fg*", &mut buf)?;
+ assert_eq!("*a_fg*", &buf);
+
+ buf.clear();
+ Serializer::serialize_key("key1", &mut buf)?;
+ assert_eq!("key1", &buf);
+
+ buf.clear();
+ Serializer::serialize_key("ke-y.1", &mut buf)?;
+ assert_eq!("ke-y.1", &buf);
+
+ Ok(())
+}
+
+#[test]
+fn serialize_key_erros() -> Result<(), Box<dyn Error>> {
+ let mut buf = String::new();
+
+ assert_eq!(
+ Err("serialize_key: disallowed character in input"),
+ Serializer::serialize_key("AND", &mut buf)
+ );
+ assert_eq!(
+ Err("serialize_key: first character is not lcalpha or '*'"),
+ Serializer::serialize_key("_key", &mut buf)
+ );
+ assert_eq!(
+ Err("serialize_key: first character is not lcalpha or '*'"),
+ Serializer::serialize_key("7key", &mut buf)
+ );
+ Ok(())
+}
+
+#[test]
+fn serialize_list_of_items_and_inner_list() -> Result<(), Box<dyn Error>> {
+ let mut buf = String::new();
+
+ let item1 = Item::new(12.into());
+ let item2 = Item::new(14.into());
+ let item3 = Item::new(BareItem::Token("a".to_owned()));
+ let item4 = Item::new(BareItem::Token("b".to_owned()));
+ let inner_list_param = Parameters::from_iter(vec![(
+ "param".to_owned(),
+ BareItem::String("param_value_1".to_owned()),
+ )]);
+ let inner_list = InnerList::with_params(vec![item3, item4], inner_list_param);
+ let input: List = vec![item1.into(), item2.into(), inner_list.into()];
+
+ Serializer::serialize_list(&input, &mut buf)?;
+ assert_eq!("12, 14, (a b);param=\"param_value_1\"", &buf);
+ Ok(())
+}
+
+#[test]
+fn serialize_list_of_lists() -> Result<(), Box<dyn Error>> {
+ let mut buf = String::new();
+
+ let item1 = Item::new(1.into());
+ let item2 = Item::new(2.into());
+ let item3 = Item::new(42.into());
+ let item4 = Item::new(43.into());
+ let inner_list_1 = InnerList::new(vec![item1, item2]);
+ let inner_list_2 = InnerList::new(vec![item3, item4]);
+ let input: List = vec![inner_list_1.into(), inner_list_2.into()];
+
+ Serializer::serialize_list(&input, &mut buf)?;
+ assert_eq!("(1 2), (42 43)", &buf);
+ Ok(())
+}
+
+#[test]
+fn serialize_list_with_bool_item_and_bool_params() -> Result<(), Box<dyn Error>> {
+ let mut buf = String::new();
+
+ let item1_params = Parameters::from_iter(vec![
+ ("a".to_owned(), BareItem::Boolean(true)),
+ ("b".to_owned(), BareItem::Boolean(false)),
+ ]);
+ let item1 = Item::with_params(BareItem::Boolean(false), item1_params);
+ let item2 = Item::new(BareItem::Token("cde_456".to_owned()));
+
+ let input: List = vec![item1.into(), item2.into()];
+ Serializer::serialize_list(&input, &mut buf)?;
+ assert_eq!("?0;a;b=?0, cde_456", &buf);
+ Ok(())
+}
+
+#[test]
+fn serialize_dictionary_with_params() -> Result<(), Box<dyn Error>> {
+ let mut buf = String::new();
+
+ let item1_params = Parameters::from_iter(vec![
+ ("a".to_owned(), 1.into()),
+ ("b".to_owned(), BareItem::Boolean(true)),
+ ]);
+ let item2_params = Parameters::new();
+ let item3_params = Parameters::from_iter(vec![
+ ("q".to_owned(), BareItem::Boolean(false)),
+ ("r".to_owned(), BareItem::String("+w".to_owned())),
+ ]);
+
+ let item1 = Item::with_params(123.into(), item1_params);
+ let item2 = Item::with_params(456.into(), item2_params);
+ let item3 = Item::with_params(789.into(), item3_params);
+
+ let input = Dictionary::from_iter(vec![
+ ("abc".to_owned(), item1.into()),
+ ("def".to_owned(), item2.into()),
+ ("ghi".to_owned(), item3.into()),
+ ]);
+
+ Serializer::serialize_dict(&input, &mut buf)?;
+ assert_eq!("abc=123;a=1;b, def=456, ghi=789;q=?0;r=\"+w\"", &buf);
+ Ok(())
+}
+
+#[test]
+fn serialize_dict_empty_member_value() -> Result<(), Box<dyn Error>> {
+ let mut buf = String::new();
+
+ let inner_list = InnerList::new(vec![]);
+ let input = Dictionary::from_iter(vec![("a".to_owned(), inner_list.into())]);
+ Serializer::serialize_dict(&input, &mut buf)?;
+ assert_eq!("a=()", &buf);
+ Ok(())
+}
diff --git a/third_party/rust/sfv/src/utils.rs b/third_party/rust/sfv/src/utils.rs
new file mode 100644
index 0000000000..7eb79340ba
--- /dev/null
+++ b/third_party/rust/sfv/src/utils.rs
@@ -0,0 +1,44 @@
+use data_encoding::{Encoding, Specification};
+use std::iter::Peekable;
+use std::str::Chars;
+
+pub(crate) fn base64() -> Result<Encoding, &'static str> {
+ let mut spec = Specification::new();
+ spec.check_trailing_bits = false;
+ spec.symbols
+ .push_str("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/");
+ spec.padding = None;
+ spec.ignore = "=".to_owned();
+ spec.encoding()
+ .map_err(|_err| "invalid base64 specification")
+}
+
+pub(crate) fn is_tchar(c: char) -> bool {
+ // See tchar values list in https://tools.ietf.org/html/rfc7230#section-3.2.6
+ let tchars = "!#$%&'*+-.^_`|~";
+ tchars.contains(c) || c.is_ascii_alphanumeric()
+}
+
+pub(crate) fn is_allowed_b64_content(c: char) -> bool {
+ c.is_ascii_alphanumeric() || c == '+' || c == '=' || c == '/'
+}
+
+pub(crate) fn consume_ows_chars(input_chars: &mut Peekable<Chars>) {
+ while let Some(c) = input_chars.peek() {
+ if c == &' ' || c == &'\t' {
+ input_chars.next();
+ } else {
+ break;
+ }
+ }
+}
+
+pub(crate) fn consume_sp_chars(input_chars: &mut Peekable<Chars>) {
+ while let Some(c) = input_chars.peek() {
+ if c == &' ' {
+ input_chars.next();
+ } else {
+ break;
+ }
+ }
+}