summaryrefslogtreecommitdiffstats
path: root/third_party/rust/sfv
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /third_party/rust/sfv
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
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.toml46
-rw-r--r--third_party/rust/sfv/LICENSE373
-rw-r--r--third_party/rust/sfv/README.md10
-rw-r--r--third_party/rust/sfv/benches/bench.rs171
-rw-r--r--third_party/rust/sfv/src/lib.rs389
-rw-r--r--third_party/rust/sfv/src/parser.rs477
-rw-r--r--third_party/rust/sfv/src/ref_serializer.rs310
-rw-r--r--third_party/rust/sfv/src/serializer.rs320
-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
12 files changed, 3522 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..6dbb398054
--- /dev/null
+++ b/third_party/rust/sfv/.cargo-checksum.json
@@ -0,0 +1 @@
+{"files":{"Cargo.toml":"75a68f4ae953f65f25e970fed50f05c43f37fdbc15a1d02079c61095bb9d18d7","LICENSE":"1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5","README.md":"a9b433dfdebdc258e280071d40bb0df840456b6d8d1464b8955706b885eace3c","benches/bench.rs":"bbc60db4b542abb3738eba80f5c7c54ac39301ed5e48e2ae2a94cecfdb42e33f","src/lib.rs":"db8bc1c9f61a424f8923a849d131c71937236b7846d40643d1c6aee952b62236","src/parser.rs":"4de9bc1e04b536357d4c635350ba0dc1fbafae4b5741f6cd47dffd904468c251","src/ref_serializer.rs":"8806ee50e2b2ae466a49788d7e972a47329c0e2c842d669673a152286e81c5d9","src/serializer.rs":"5d7a4d18a4508d433993b6a7ee405285ed2b33cbc6b84101cc4720c897f5586e","src/test_parser.rs":"7a2728e7cbdcb1f3bb42e009045ec0dcfca241316a2aee4905925d4b1ce0bb3a","src/test_serializer.rs":"2419279c9a9a4f48952836d63f3822281c18691d86c146749a573c52a41d6ff0","src/utils.rs":"94c8f79f4747973819b9da2c1a9f6246bf3b5ea7450b376a98eb055f6acf8e73"},"package":"13ed1dd5a626253083678d21b5c38dd94f8717b961d4b7469eb96b41173cc148"} \ 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..4b989dd8bb
--- /dev/null
+++ b/third_party/rust/sfv/Cargo.toml
@@ -0,0 +1,46 @@
+# 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 believe there's an error in this file please file an
+# issue against the rust-lang/cargo repository. If you're
+# editing this file be aware that the upstream Cargo.toml
+# will likely look very different (and much more reasonable)
+
+[package]
+edition = "2018"
+name = "sfv"
+version = "0.8.0"
+authors = ["Tania Batieva <yalyna.ts@gmail.com>"]
+exclude = ["tests/**", ".github/*"]
+description = "Structured HTTP field values parser.\nImplementation of IETF draft https://httpwg.org/http-extensions/draft-ietf-httpbis-header-structure.html"
+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.2.1"
+
+[dependencies.indexmap]
+version = "1.1.0"
+
+[dependencies.rust_decimal]
+version = "1.6.0"
+[dev-dependencies.criterion]
+version = "0.3.3"
+
+[dev-dependencies.serde]
+version = "1.0"
+features = ["derive"]
+
+[dev-dependencies.serde_json]
+version = "1.0"
+features = ["preserve_order"]
diff --git a/third_party/rust/sfv/LICENSE b/third_party/rust/sfv/LICENSE
new file mode 100644
index 0000000000..a612ad9813
--- /dev/null
+++ b/third_party/rust/sfv/LICENSE
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+ means each individual or legal entity that creates, contributes to
+ the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+ means the combination of the Contributions of others (if any) used
+ by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+ means Source Code Form to which the initial Contributor has attached
+ the notice in Exhibit A, the Executable Form of such Source Code
+ Form, and Modifications of such Source Code Form, in each case
+ including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ (a) that the initial Contributor has attached the notice described
+ in Exhibit B to the Covered Software; or
+
+ (b) that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the
+ terms of a Secondary License.
+
+1.6. "Executable Form"
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+ means a work that combines Covered Software with other material, in
+ a separate file or files, that is not Covered Software.
+
+1.8. "License"
+ means this document.
+
+1.9. "Licensable"
+ means having the right to grant, to the maximum extent possible,
+ whether at the time of the initial grant or subsequently, any and
+ all of the rights conveyed by this License.
+
+1.10. "Modifications"
+ means any of the following:
+
+ (a) any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered
+ Software; or
+
+ (b) any new file in Source Code Form that contains any Covered
+ Software.
+
+1.11. "Patent Claims" of a Contributor
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the
+ License, by the making, using, selling, offering for sale, having
+ made, import, or transfer of either its Contributions or its
+ Contributor Version.
+
+1.12. "Secondary License"
+ means either the GNU General Public License, Version 2.0, the GNU
+ Lesser General Public License, Version 2.1, the GNU Affero General
+ Public License, Version 3.0, or any later versions of those
+ licenses.
+
+1.13. "Source Code Form"
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that
+ controls, is controlled by, or is under common control with You. For
+ purposes of this definition, "control" means (a) the power, direct
+ or indirect, to cause the direction or management of such entity,
+ whether by contract or otherwise, or (b) ownership of more than
+ fifty percent (50%) of the outstanding shares or beneficial
+ ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+ for sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+ or
+
+(b) for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+ Form, as described in Section 3.1, and You must inform recipients of
+ the Executable Form how they can obtain a copy of such Source Code
+ Form by reasonable means in a timely manner, at a charge no more
+ than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter
+ the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+* *
+* 6. Disclaimer of Warranty *
+* ------------------------- *
+* *
+* Covered Software is provided under this License on an "as is" *
+* basis, without warranty of any kind, either expressed, implied, or *
+* statutory, including, without limitation, warranties that the *
+* Covered Software is free of defects, merchantable, fit for a *
+* particular purpose or non-infringing. The entire risk as to the *
+* quality and performance of the Covered Software is with You. *
+* Should any Covered Software prove defective in any respect, You *
+* (not any Contributor) assume the cost of any necessary servicing, *
+* repair, or correction. This disclaimer of warranty constitutes an *
+* essential part of this License. No use of any Covered Software is *
+* authorized under this License except under this disclaimer. *
+* *
+************************************************************************
+
+************************************************************************
+* *
+* 7. Limitation of Liability *
+* -------------------------- *
+* *
+* Under no circumstances and under no legal theory, whether tort *
+* (including negligence), contract, or otherwise, shall any *
+* Contributor, or anyone who distributes Covered Software as *
+* permitted above, be liable to You for any direct, indirect, *
+* special, incidental, or consequential damages of any character *
+* including, without limitation, damages for lost profits, loss of *
+* goodwill, work stoppage, computer failure or malfunction, or any *
+* and all other commercial damages or losses, even if such party *
+* shall have been informed of the possibility of such damages. This *
+* limitation of liability shall not apply to liability for death or *
+* personal injury resulting from such party's negligence to the *
+* extent applicable law prohibits such limitation. Some *
+* jurisdictions do not allow the exclusion or limitation of *
+* incidental or consequential damages, so this exclusion and *
+* limitation may not apply to You. *
+* *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+ This Source Code Form is "Incompatible With Secondary Licenses", as
+ defined by the Mozilla Public License, v. 2.0.
diff --git a/third_party/rust/sfv/README.md b/third_party/rust/sfv/README.md
new file mode 100644
index 0000000000..f1e431b3d4
--- /dev/null
+++ b/third_party/rust/sfv/README.md
@@ -0,0 +1,10 @@
+![Build Status](https://github.com/undef1nd/structured-headers/workflows/CI/badge.svg)
+[![Version](https://img.shields.io/crates/v/sfv.svg)](https://crates.io/crates/sfv)
+
+# Structured Field Values for HTTP
+
+[Documentation](https://docs.rs/sfv/)
+
+`sfv` crate is an implementation of IETF draft [Structured Field Values for HTTP](https://httpwg.org/http-extensions/draft-ietf-httpbis-header-structure.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.
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..8fa6b2fc65
--- /dev/null
+++ b/third_party/rust/sfv/src/lib.rs
@@ -0,0 +1,389 @@
+/*!
+`sfv` crate is an implementation of IETF draft [Structured Field Values for HTTP](https://httpwg.org/http-extensions/draft-ietf-httpbis-header-structure.html)
+for parsing and serializing structured 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);
+
+```
+
+### Value Creation 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..c018a60695
--- /dev/null
+++ b/third_party/rust/sfv/src/parser.rs
@@ -0,0 +1,477 @@
+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/http-extensions/draft-ietf-httpbis-header-structure.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/http-extensions/draft-ietf-httpbis-header-structure.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/http-extensions/draft-ietf-httpbis-header-structure.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/http-extensions/draft-ietf-httpbis-header-structure.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/http-extensions/draft-ietf-httpbis-header-structure.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(mut input_chars: &mut Peekable<Chars>) -> SFVResult<BareItem> {
+ // https://httpwg.org/http-extensions/draft-ietf-httpbis-header-structure.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(&mut input_chars)?)),
+ Some(&'"') => Ok(BareItem::String(Self::parse_string(&mut input_chars)?)),
+ Some(&':') => Ok(BareItem::ByteSeq(Self::parse_byte_sequence(
+ &mut input_chars,
+ )?)),
+ Some(&c) if c == '*' || c.is_ascii_alphabetic() => {
+ Ok(BareItem::Token(Self::parse_token(&mut input_chars)?))
+ }
+ Some(&c) if c == '-' || c.is_ascii_digit() => {
+ match Self::parse_number(&mut 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/http-extensions/draft-ietf-httpbis-header-structure.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/http-extensions/draft-ietf-httpbis-header-structure.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/http-extensions/draft-ietf-httpbis-header-structure.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/http-extensions/draft-ietf-httpbis-header-structure.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/http-extensions/draft-ietf-httpbis-header-structure.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/http-extensions/draft-ietf-httpbis-header-structure.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..f490aaf9dd
--- /dev/null
+++ b/third_party/rust/sfv/src/serializer.rs
@@ -0,0 +1,320 @@
+use crate::utils;
+use crate::{
+ BareItem, Decimal, Dictionary, InnerList, Item, List, ListEntry, Parameters, RefBareItem,
+ SFVResult,
+};
+use data_encoding::BASE64;
+use rust_decimal::prelude::Zero;
+
+/// 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/http-extensions/draft-ietf-httpbis-header-structure.html#ser-item
+
+ Self::serialize_bare_item(&input_item.bare_item, output)?;
+ Self::serialize_parameters(&input_item.params, output)?;
+ Ok(())
+ }
+
+ #[deny(clippy::ptr_arg)]
+ pub(crate) fn serialize_list(input_list: &List, output: &mut String) -> SFVResult<()> {
+ // https://httpwg.org/http-extensions/draft-ietf-httpbis-header-structure.html#ser-list
+ if input_list.len() == 0 {
+ 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/http-extensions/draft-ietf-httpbis-header-structure.html#ser-dictionary
+ if input_dict.len() == 0 {
+ 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/http-extensions/draft-ietf-httpbis-header-structure.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_str(" ");
+ }
+ }
+ 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/http-extensions/draft-ietf-httpbis-header-structure.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/http-extensions/draft-ietf-httpbis-header-structure.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/http-extensions/draft-ietf-httpbis-header-structure.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/http-extensions/draft-ietf-httpbis-header-structure.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/http-extensions/draft-ietf-httpbis-header-structure.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/http-extensions/draft-ietf-httpbis-header-structure.html#ser-integer
+
+ if !value.is_ascii() {
+ return Err("serialize_string: non-ascii character");
+ }
+
+ let vchar_or_sp = |char| char == '\x7f' || (char >= '\x00' && char <= '\x1f');
+ 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/http-extensions/draft-ietf-httpbis-header-structure.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/http-extensions/draft-ietf-httpbis-header-structure.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/http-extensions/draft-ietf-httpbis-header-structure.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;
+ }
+ }
+}