base64url_nopad

base64url without padding library.
git clone https://git.philomathiclife.com/repos/base64url_nopad
Log | Files | Refs | README

commit ab3ee50ee16c736370e8511497c661fdc915b190
Author: Zack Newman <zack@philomathiclife.com>
Date:   Fri,  8 Aug 2025 14:05:35 -0600

init

Diffstat:
A.gitignore | 2++
ACargo.toml | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ALICENSE-APACHE | 177+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ALICENSE-MIT | 20++++++++++++++++++++
AREADME.md | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib.rs | 2096+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 2484 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,2 @@ +Cargo.lock +target/** diff --git a/Cargo.toml b/Cargo.toml @@ -0,0 +1,102 @@ +[package] +authors = ["Zack Newman <zack@philomathiclife.com>"] +categories = ["encoding", "no-std::no-alloc"] +description = "Efficient and correct base64url without padding encoding and decoding" +documentation = "https://docs.rs/base64url_nopad/latest/base64url_nopad/" +edition = "2024" +keywords = ["base64", "base64url", "no-std"] +license = "MIT OR Apache-2.0" +name = "base64url_nopad" +readme = "README.md" +repository = "https://git.philomathiclife.com/repos/base64url_nopad/" +rust-version = "1.89.0" +version = "0.1.0" + +[lints.rust] +ambiguous_negative_literals = { level = "deny", priority = -1 } +closure_returning_async_block = { level = "deny", priority = -1 } +deprecated_safe = { level = "deny", priority = -1 } +deref_into_dyn_supertrait = { level = "deny", priority = -1 } +ffi_unwind_calls = { level = "deny", priority = -1 } +future_incompatible = { level = "deny", priority = -1 } +#fuzzy_provenance_casts = { level = "deny", priority = -1 } +impl_trait_redundant_captures = { level = "deny", priority = -1 } +keyword_idents = { level = "deny", priority = -1 } +let_underscore = { level = "deny", priority = -1 } +linker_messages = { level = "deny", priority = -1 } +#lossy_provenance_casts = { level = "deny", priority = -1 } +macro_use_extern_crate = { level = "deny", priority = -1 } +meta_variable_misuse = { level = "deny", priority = -1 } +missing_copy_implementations = { level = "deny", priority = -1 } +missing_debug_implementations = { level = "deny", priority = -1 } +missing_docs = { level = "deny", priority = -1 } +#multiple_supertrait_upcastable = { level = "deny", priority = -1 } +#must_not_suspend = { level = "deny", priority = -1 } +non_ascii_idents = { level = "deny", priority = -1 } +#non_exhaustive_omitted_patterns = { level = "deny", priority = -1 } +nonstandard_style = { level = "deny", priority = -1 } +redundant_imports = { level = "deny", priority = -1 } +redundant_lifetimes = { level = "deny", priority = -1 } +refining_impl_trait = { level = "deny", priority = -1 } +rust_2018_compatibility = { level = "deny", priority = -1 } +rust_2018_idioms = { level = "deny", priority = -1 } +rust_2021_compatibility = { level = "deny", priority = -1 } +rust_2024_compatibility = { level = "deny", priority = -1 } +single_use_lifetimes = { level = "deny", priority = -1 } +#supertrait_item_shadowing_definition = { level = "deny", priority = -1 } +trivial_casts = { level = "deny", priority = -1 } +trivial_numeric_casts = { level = "deny", priority = -1 } +unit_bindings = { level = "deny", priority = -1 } +unnameable_types = { level = "deny", priority = -1 } +#unqualified_local_imports = { level = "deny", priority = -1 } +unreachable_pub = { level = "deny", priority = -1 } +unsafe_code = { level = "deny", priority = -1 } +unstable_features = { level = "deny", priority = -1 } +unused = { level = "deny", priority = -1 } +unused_crate_dependencies = { level = "deny", priority = -1 } +unused_import_braces = { level = "deny", priority = -1 } +unused_lifetimes = { level = "deny", priority = -1 } +unused_qualifications = { level = "deny", priority = -1 } +unused_results = { level = "deny", priority = -1 } +variant_size_differences = { level = "deny", priority = -1 } +warnings = { level = "deny", priority = -1 } + +[lints.clippy] +all = { level = "deny", priority = -1 } +cargo = { level = "deny", priority = -1 } +complexity = { level = "deny", priority = -1 } +correctness = { level = "deny", priority = -1 } +nursery = { level = "deny", priority = -1 } +pedantic = { level = "deny", priority = -1 } +perf = { level = "deny", priority = -1 } +restriction = { level = "deny", priority = -1 } +style = { level = "deny", priority = -1 } +suspicious = { level = "deny", priority = -1 } +# Noisy, opinionated, and likely don't prevent bugs or improve APIs. +arbitrary_source_item_ordering = "allow" +blanket_clippy_restriction_lints = "allow" +exhaustive_enums = "allow" +implicit_return = "allow" +min_ident_chars = "allow" +missing_trait_methods = "allow" +question_mark_used = "allow" +ref_patterns = "allow" +return_and_then = "allow" +single_char_lifetime_names = "allow" +unseparated_literal_suffix = "allow" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dev-dependencies] +rand = { version = "0.9.2", default-features = false, features = ["os_rng", "small_rng"] } + + +### FEATURES ################################################################# + +[features] +default = ["alloc"] + +# Provide functionality that requires memory allocations via the alloc crate. +alloc = [] diff --git a/LICENSE-APACHE b/LICENSE-APACHE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/LICENSE-MIT b/LICENSE-MIT @@ -0,0 +1,20 @@ +Copyright © 2025 Zack Newman + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +“Software”), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md @@ -0,0 +1,87 @@ +# `base64url_nopad` + +[<img alt="git" src="https://git.philomathiclife.com/badges/base64url_nopad.svg" height="20">](https://git.philomathiclife.com/base64url_nopad/log.html) +[<img alt="crates.io" src="https://img.shields.io/crates/v/base64url_nopad.svg?style=for-the-badge&color=fc8d62&logo=rust" height="20">](https://crates.io/crates/base64url_nopad) +[<img alt="docs.rs" src="https://img.shields.io/badge/docs.rs-base64url_nopad-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs" height="20">](https://docs.rs/base64url_nopad/latest/base64url_nopad/) + +`base64url_nopad` is a library for efficient and correct encoding and decoding of base64url without padding +data. All functions that can be `const` are `const`. Great care is made to ensure _all_ arithmetic is free +from "side effects" (e.g., overflow). `panic`s are avoided at all costs unless explicitly documented +_including_ `panic`s related to memory allocations. + +## `base64url_nopad` in action + +```rust +use base64url_nopad::DecodeErr; +/// Length of our input to encode. +const INPUT_LEN: usize = 259; +/// The base64url encoded value without padding of our input. +const ENCODED_VAL: &str = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0-P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn-AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq-wsbKztLW2t7i5uru8vb6_wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t_g4eLj5OXm5-jp6uvs7e7v8PHy8_T19vf4-fr7_P3-_1MH0Q"; +fn main() -> Result<(), DecodeErr> { + let mut input = [0; INPUT_LEN]; + for i in 0..=255 { + input[usize::from(i)] = i; + } + input[256] = 83; + input[257] = 7; + input[258] = 209; + let mut output = [0; base64url_nopad::encode_len(INPUT_LEN)]; + assert_eq!(base64url_nopad::encode_buffer(&input, &mut output), ENCODED_VAL); + assert!(base64url_nopad::decode_len(output.len()).is_some_and(|len| len == INPUT_LEN)); + base64url_nopad::decode_buffer(ENCODED_VAL.as_bytes(), &mut output[..INPUT_LEN])?; + assert_eq!(input, output[..INPUT_LEN]); +} +``` + +## Cargo "features" + +### `alloc` + +Enables support for memory allocations via [`alloc`](https://doc.rust-lang.org/alloc/). + +## Correctness of code + +This library is written in a way that is free from any overflow, underflow, or other kinds of +"arithmetic side effects". All functions that can `panic` are explicitly documented as such; and all +possible `panic`s are isolated to convenience functions that `panic` instead of error. Strict encoding and +decoding is performed; thus if an input contains _any_ invalid data, it is guaranteed to fail when decoding +it (e.g., trailing non-zero bits). + +## Minimum Supported Rust Version (MSRV) + +This will frequently be updated to be the same as stable. Specifically, any time stable is updated and that +update has "useful" features or compilation no longer succeeds (e.g., due to new compiler lints), then MSRV +will be updated. + +MSRV changes will correspond to a SemVer patch version bump pre-`1.0.0`; otherwise a minor version bump. + +## SemVer Policy + +* All on-by-default features of this library are covered by SemVer +* MSRV is considered exempt from SemVer as noted above + +## License + +Licensed under either of + +* Apache License, Version 2.0 ([LICENSE-APACHE](https://www.apache.org/licenses/LICENSE-2.0)) +* MIT license ([LICENSE-MIT](https://opensource.org/licenses/MIT)) + +at your option. + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, +as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. + +Before any PR is sent, `cargo clippy` and `cargo t` should be run _for each possible combination of "features"_ +using stable Rust. One easy way to achieve this is by building `ci` and invoking it with no commands in the +`base64url_nopad` directory or sub-directories. You can fetch `ci` via +`git clone https://git.philomathiclife.com/repos/ci`, and it can be built with `cargo build --release`. +Additionally, `RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features` should be run to ensure +documentation can be built. + +### Status + +This package is actively maintained. It is only tested on `x86_64-unknown-linux-gnu`, `x86_64-unknown-openbsd`, +and `aarch64-apple-darwin` targets; but it should work on most platforms. diff --git a/src/lib.rs b/src/lib.rs @@ -0,0 +1,2096 @@ +//! [![git]](https://git.philomathiclife.com/base64url_nopad/log.html)&ensp;[![crates-io]](https://crates.io/crates/base64url_nopad)&ensp;[![docs-rs]](crate) +//! +//! [git]: https://git.philomathiclife.com/git_badge.svg +//! [crates-io]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust +//! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs +//! +//! `base64url_nopad` is a library for efficient and correct encoding and decoding of base64url without padding +//! data. All functions that can be `const` are `const`. Great care is made to ensure _all_ arithmetic is free +//! from "side effects" (e.g., overflow). `panic`s are avoided at all costs unless explicitly documented +//! _including_ `panic`s related to memory allocations. +//! +//! ## `base64url_nopad` in action +//! +//! ``` +//! # use base64url_nopad::DecodeErr; +//! /// Length of our input to encode. +//! const INPUT_LEN: usize = 259; +//! /// The base64url encoded value without padding of our input. +//! const ENCODED_VAL: &str = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0-P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn-AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq-wsbKztLW2t7i5uru8vb6_wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t_g4eLj5OXm5-jp6uvs7e7v8PHy8_T19vf4-fr7_P3-_1MH0Q"; +//! let mut input = [0; INPUT_LEN]; +//! for i in 0..=255 { +//! input[usize::from(i)] = i; +//! } +//! input[256] = 83; +//! input[257] = 7; +//! input[258] = 209; +//! let mut output = [0; base64url_nopad::encode_len(INPUT_LEN)]; +//! assert_eq!(base64url_nopad::encode_buffer(&input, &mut output), ENCODED_VAL); +//! assert!(base64url_nopad::decode_len(output.len()).is_some_and(|len| len == INPUT_LEN)); +//! base64url_nopad::decode_buffer(ENCODED_VAL.as_bytes(), &mut output[..INPUT_LEN])?; +//! assert_eq!(input, output[..INPUT_LEN]); +//! # Ok::<_, DecodeErr>(()) +//! ``` +//! +//! ## Cargo "features" +//! +//! ### `alloc` +//! +//! Enables support for memory allocations via [`alloc`]. +//! +//! ## Correctness of code +//! +//! This library is written in a way that is free from any overflow, underflow, or other kinds of +//! "arithmetic side effects". All functions that can `panic` are explicitly documented as such; and all +//! possible `panic`s are isolated to convenience functions that `panic` instead of error. Strict encoding and +//! decoding is performed; thus if an input contains _any_ invalid data, it is guaranteed to fail when decoding +//! it (e.g., trailing non-zero bits). +#![no_std] +#![cfg_attr(docsrs, feature(doc_cfg))] +#[cfg(any(doc, feature = "alloc"))] +extern crate alloc; +#[cfg(any(doc, feature = "alloc"))] +use alloc::{collections::TryReserveError, string::String, vec::Vec}; +use core::{ + error::Error, + fmt::{self, Display, Formatter, Write}, + mem, +}; +/// `b'A'`. +const UPPER_A: u8 = b'A'; +/// `b'B'`. +const UPPER_B: u8 = b'B'; +/// `b'C'`. +const UPPER_C: u8 = b'C'; +/// `b'D'`. +const UPPER_D: u8 = b'D'; +/// `b'E'`. +const UPPER_E: u8 = b'E'; +/// `b'F'`. +const UPPER_F: u8 = b'F'; +/// `b'G'`. +const UPPER_G: u8 = b'G'; +/// `b'H'`. +const UPPER_H: u8 = b'H'; +/// `b'I'`. +const UPPER_I: u8 = b'I'; +/// `b'J'`. +const UPPER_J: u8 = b'J'; +/// `b'K'`. +const UPPER_K: u8 = b'K'; +/// `b'L'`. +const UPPER_L: u8 = b'L'; +/// `b'M'`. +const UPPER_M: u8 = b'M'; +/// `b'N'`. +const UPPER_N: u8 = b'N'; +/// `b'O'`. +const UPPER_O: u8 = b'O'; +/// `b'P'`. +const UPPER_P: u8 = b'P'; +/// `b'Q'`. +const UPPER_Q: u8 = b'Q'; +/// `b'R'`. +const UPPER_R: u8 = b'R'; +/// `b'S'`. +const UPPER_S: u8 = b'S'; +/// `b'T'`. +const UPPER_T: u8 = b'T'; +/// `b'U'`. +const UPPER_U: u8 = b'U'; +/// `b'V'`. +const UPPER_V: u8 = b'V'; +/// `b'W'`. +const UPPER_W: u8 = b'W'; +/// `b'X'`. +const UPPER_X: u8 = b'X'; +/// `b'Y'`. +const UPPER_Y: u8 = b'Y'; +/// `b'Z'`. +const UPPER_Z: u8 = b'Z'; +/// `b'a'`. +const LOWER_A: u8 = b'a'; +/// `b'b'`. +const LOWER_B: u8 = b'b'; +/// `b'c'`. +const LOWER_C: u8 = b'c'; +/// `b'd'`. +const LOWER_D: u8 = b'd'; +/// `b'e'`. +const LOWER_E: u8 = b'e'; +/// `b'f'`. +const LOWER_F: u8 = b'f'; +/// `b'g'`. +const LOWER_G: u8 = b'g'; +/// `b'h'`. +const LOWER_H: u8 = b'h'; +/// `b'i'`. +const LOWER_I: u8 = b'i'; +/// `b'j'`. +const LOWER_J: u8 = b'j'; +/// `b'k'`. +const LOWER_K: u8 = b'k'; +/// `b'l'`. +const LOWER_L: u8 = b'l'; +/// `b'm'`. +const LOWER_M: u8 = b'm'; +/// `b'n'`. +const LOWER_N: u8 = b'n'; +/// `b'o'`. +const LOWER_O: u8 = b'o'; +/// `b'p'`. +const LOWER_P: u8 = b'p'; +/// `b'q'`. +const LOWER_Q: u8 = b'q'; +/// `b'r'`. +const LOWER_R: u8 = b'r'; +/// `b's'`. +const LOWER_S: u8 = b's'; +/// `b't'`. +const LOWER_T: u8 = b't'; +/// `b'u'`. +const LOWER_U: u8 = b'u'; +/// `b'v'`. +const LOWER_V: u8 = b'v'; +/// `b'w'`. +const LOWER_W: u8 = b'w'; +/// `b'x'`. +const LOWER_X: u8 = b'x'; +/// `b'y'`. +const LOWER_Y: u8 = b'y'; +/// `b'z'`. +const LOWER_Z: u8 = b'z'; +/// `b'0'`. +const ZERO: u8 = b'0'; +/// `b'1'`. +const ONE: u8 = b'1'; +/// `b'2'`. +const TWO: u8 = b'2'; +/// `b'3'`. +const THREE: u8 = b'3'; +/// `b'4'`. +const FOUR: u8 = b'4'; +/// `b'5'`. +const FIVE: u8 = b'5'; +/// `b'6'`. +const SIX: u8 = b'6'; +/// `b'7'`. +const SEVEN: u8 = b'7'; +/// `b'8'`. +const EIGHT: u8 = b'8'; +/// `b'9'`. +const NINE: u8 = b'9'; +/// `b'-'`. +const HYPHEN: u8 = b'-'; +/// `b'_'`. +const UNDERSCORE: u8 = b'_'; +/// `'A'`. +const UPPER_A_CHAR: char = 'A'; +/// `'B'`. +const UPPER_B_CHAR: char = 'B'; +/// `'C'`. +const UPPER_C_CHAR: char = 'C'; +/// `'D'`. +const UPPER_D_CHAR: char = 'D'; +/// `'E'`. +const UPPER_E_CHAR: char = 'E'; +/// `'F'`. +const UPPER_F_CHAR: char = 'F'; +/// `'G'`. +const UPPER_G_CHAR: char = 'G'; +/// `'H'`. +const UPPER_H_CHAR: char = 'H'; +/// `'I'`. +const UPPER_I_CHAR: char = 'I'; +/// `'J'`. +const UPPER_J_CHAR: char = 'J'; +/// `'K'`. +const UPPER_K_CHAR: char = 'K'; +/// `'L'`. +const UPPER_L_CHAR: char = 'L'; +/// `'M'`. +const UPPER_M_CHAR: char = 'M'; +/// `'N'`. +const UPPER_N_CHAR: char = 'N'; +/// `'O'`. +const UPPER_O_CHAR: char = 'O'; +/// `'P'`. +const UPPER_P_CHAR: char = 'P'; +/// `'Q'`. +const UPPER_Q_CHAR: char = 'Q'; +/// `'R'`. +const UPPER_R_CHAR: char = 'R'; +/// `'S'`. +const UPPER_S_CHAR: char = 'S'; +/// `'T'`. +const UPPER_T_CHAR: char = 'T'; +/// `'U'`. +const UPPER_U_CHAR: char = 'U'; +/// `'V'`. +const UPPER_V_CHAR: char = 'V'; +/// `'W'`. +const UPPER_W_CHAR: char = 'W'; +/// `'X'`. +const UPPER_X_CHAR: char = 'X'; +/// `'Y'`. +const UPPER_Y_CHAR: char = 'Y'; +/// `'Z'`. +const UPPER_Z_CHAR: char = 'Z'; +/// `'a'`. +const LOWER_A_CHAR: char = 'a'; +/// `'b'`. +const LOWER_B_CHAR: char = 'b'; +/// `'c'`. +const LOWER_C_CHAR: char = 'c'; +/// `'d'`. +const LOWER_D_CHAR: char = 'd'; +/// `'e'`. +const LOWER_E_CHAR: char = 'e'; +/// `'f'`. +const LOWER_F_CHAR: char = 'f'; +/// `'g'`. +const LOWER_G_CHAR: char = 'g'; +/// `'h'`. +const LOWER_H_CHAR: char = 'h'; +/// `'i'`. +const LOWER_I_CHAR: char = 'i'; +/// `'j'`. +const LOWER_J_CHAR: char = 'j'; +/// `'k'`. +const LOWER_K_CHAR: char = 'k'; +/// `'l'`. +const LOWER_L_CHAR: char = 'l'; +/// `'m'`. +const LOWER_M_CHAR: char = 'm'; +/// `'n'`. +const LOWER_N_CHAR: char = 'n'; +/// `'o'`. +const LOWER_O_CHAR: char = 'o'; +/// `'p'`. +const LOWER_P_CHAR: char = 'p'; +/// `'q'`. +const LOWER_Q_CHAR: char = 'q'; +/// `'r'`. +const LOWER_R_CHAR: char = 'r'; +/// `'s'`. +const LOWER_S_CHAR: char = 's'; +/// `'t'`. +const LOWER_T_CHAR: char = 't'; +/// `'u'`. +const LOWER_U_CHAR: char = 'u'; +/// `'v'`. +const LOWER_V_CHAR: char = 'v'; +/// `'w'`. +const LOWER_W_CHAR: char = 'w'; +/// `'x'`. +const LOWER_X_CHAR: char = 'x'; +/// `'y'`. +const LOWER_Y_CHAR: char = 'y'; +/// `'z'`. +const LOWER_Z_CHAR: char = 'z'; +/// `'0'`. +const ZERO_CHAR: char = '0'; +/// `'1'`. +const ONE_CHAR: char = '1'; +/// `'2'`. +const TWO_CHAR: char = '2'; +/// `'3'`. +const THREE_CHAR: char = '3'; +/// `'4'`. +const FOUR_CHAR: char = '4'; +/// `'5'`. +const FIVE_CHAR: char = '5'; +/// `'6'`. +const SIX_CHAR: char = '6'; +/// `'7'`. +const SEVEN_CHAR: char = '7'; +/// `'8'`. +const EIGHT_CHAR: char = '8'; +/// `'9'`. +const NINE_CHAR: char = '9'; +/// `'-'`. +const HYPHEN_CHAR: char = '-'; +/// `'_'`. +const UNDERSCORE_CHAR: char = '_'; +/// The base64url alphabet. +#[expect( + non_camel_case_types, + reason = "want to use a variant as close to what the value is" +)] +#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)] +#[repr(u8)] +pub enum Alphabet { + /// A. + #[default] + A, + /// B. + B, + /// C. + C, + /// D. + D, + /// E. + E, + /// F. + F, + /// G. + G, + /// H. + H, + /// I. + I, + /// J. + J, + /// K. + K, + /// L. + L, + /// M. + M, + /// N. + N, + /// O. + O, + /// P. + P, + /// Q. + Q, + /// R. + R, + /// S. + S, + /// T. + T, + /// U. + U, + /// V. + V, + /// W. + W, + /// X. + X, + /// Y. + Y, + /// Z. + Z, + /// a. + a, + /// b. + b, + /// c. + c, + /// d. + d, + /// e. + e, + /// f. + f, + /// g. + g, + /// h. + h, + /// i. + i, + /// j. + j, + /// k. + k, + /// l. + l, + /// m. + m, + /// n. + n, + /// o. + o, + /// p. + p, + /// q. + q, + /// r. + r, + /// s. + s, + /// t. + t, + /// u. + u, + /// v. + v, + /// w. + w, + /// x. + x, + /// y. + y, + /// z. + z, + /// 0. + Zero, + /// 1. + One, + /// 2. + Two, + /// 3. + Three, + /// 4. + Four, + /// 5. + Five, + /// 6. + Six, + /// 7. + Seven, + /// 8. + Eight, + /// 9. + Nine, + /// -. + Hyphen, + /// _. + Underscore, +} +impl Alphabet { + /// Returns `Self` that corresponds to `b`. + /// + /// `Some` is returned iff `b` is in `0..=63`. + /// + /// # Examples + /// + /// ``` + /// # use base64url_nopad::Alphabet; + /// assert!(Alphabet::from_u8(25).is_some_and(|val| val == Alphabet::Z)); + /// for i in 0..=63 { + /// assert!(Alphabet::from_u8(i).is_some()); + /// } + /// for i in 64..=255 { + /// assert!(Alphabet::from_u8(i).is_none()); + /// } + /// ``` + #[expect(unsafe_code, reason = "comment justifies correctness")] + #[expect(clippy::as_conversions, reason = "comment justifies correctness")] + #[inline] + #[must_use] + pub const fn from_u8(b: u8) -> Option<Self> { + // `Self` is `repr(u8)` and all `u8`s are valid from 0 until the maximum value + // represented by `Self::Underscore`. + if b <= Self::Underscore as u8 { + // SAFETY: + // Just checked that `b` is in-range + Some(unsafe { Self::from_u8_unchecked(b) }) + } else { + None + } + } + /// # Safety: + /// + /// `b` must be in `0..=63`, or else this is UB. + #[expect(unsafe_code, reason = "comment justifies correctness")] + const unsafe fn from_u8_unchecked(b: u8) -> Self { + // SAFETY: + // Our safety precondition is that `b` is in-range. + unsafe { mem::transmute(b) } + } + /// Returns the `u8` `self` represents. + /// + /// # Examples + /// + /// ``` + /// # use base64url_nopad::Alphabet; + /// assert_eq!(Alphabet::Hyphen.to_u8(), 62); + /// assert_eq!(Alphabet::Eight.to_u8(), Alphabet::Eight as u8); + /// ``` + #[expect(clippy::as_conversions, reason = "comment justifies correctness")] + #[inline] + #[must_use] + pub const fn to_u8(self) -> u8 { + // `Self` is `repr(u8)`; thus this is correct. + self as u8 + } + /// Returns the ASCII representation of `self`. + /// + /// # Examples + /// + /// ``` + /// # use base64url_nopad::Alphabet; + /// assert_eq!(Alphabet::c.to_ascii(), b'c'); + /// ``` + #[inline] + #[must_use] + pub const fn to_ascii(self) -> u8 { + match self { + Self::A => UPPER_A, + Self::B => UPPER_B, + Self::C => UPPER_C, + Self::D => UPPER_D, + Self::E => UPPER_E, + Self::F => UPPER_F, + Self::G => UPPER_G, + Self::H => UPPER_H, + Self::I => UPPER_I, + Self::J => UPPER_J, + Self::K => UPPER_K, + Self::L => UPPER_L, + Self::M => UPPER_M, + Self::N => UPPER_N, + Self::O => UPPER_O, + Self::P => UPPER_P, + Self::Q => UPPER_Q, + Self::R => UPPER_R, + Self::S => UPPER_S, + Self::T => UPPER_T, + Self::U => UPPER_U, + Self::V => UPPER_V, + Self::W => UPPER_W, + Self::X => UPPER_X, + Self::Y => UPPER_Y, + Self::Z => UPPER_Z, + Self::a => LOWER_A, + Self::b => LOWER_B, + Self::c => LOWER_C, + Self::d => LOWER_D, + Self::e => LOWER_E, + Self::f => LOWER_F, + Self::g => LOWER_G, + Self::h => LOWER_H, + Self::i => LOWER_I, + Self::j => LOWER_J, + Self::k => LOWER_K, + Self::l => LOWER_L, + Self::m => LOWER_M, + Self::n => LOWER_N, + Self::o => LOWER_O, + Self::p => LOWER_P, + Self::q => LOWER_Q, + Self::r => LOWER_R, + Self::s => LOWER_S, + Self::t => LOWER_T, + Self::u => LOWER_U, + Self::v => LOWER_V, + Self::w => LOWER_W, + Self::x => LOWER_X, + Self::y => LOWER_Y, + Self::z => LOWER_Z, + Self::Zero => ZERO, + Self::One => ONE, + Self::Two => TWO, + Self::Three => THREE, + Self::Four => FOUR, + Self::Five => FIVE, + Self::Six => SIX, + Self::Seven => SEVEN, + Self::Eight => EIGHT, + Self::Nine => NINE, + Self::Hyphen => HYPHEN, + Self::Underscore => UNDERSCORE, + } + } + /// Returns `Some` iff `ascii` is the ASCII representation of `Self`. + /// + /// # Examples + /// + /// ``` + /// # use base64url_nopad::Alphabet; + /// for i in 0u8..=255 { + /// if i.is_ascii_alphanumeric() || i == b'-' || i == b'_' { + /// assert!(Alphabet::from_ascii(i).is_some()); + /// } else { + /// assert!(Alphabet::from_ascii(i).is_none()); + /// } + /// } + /// ``` + #[inline] + #[must_use] + pub const fn from_ascii(ascii: u8) -> Option<Self> { + match ascii { + UPPER_A => Some(Self::A), + UPPER_B => Some(Self::B), + UPPER_C => Some(Self::C), + UPPER_D => Some(Self::D), + UPPER_E => Some(Self::E), + UPPER_F => Some(Self::F), + UPPER_G => Some(Self::G), + UPPER_H => Some(Self::H), + UPPER_I => Some(Self::I), + UPPER_J => Some(Self::J), + UPPER_K => Some(Self::K), + UPPER_L => Some(Self::L), + UPPER_M => Some(Self::M), + UPPER_N => Some(Self::N), + UPPER_O => Some(Self::O), + UPPER_P => Some(Self::P), + UPPER_Q => Some(Self::Q), + UPPER_R => Some(Self::R), + UPPER_S => Some(Self::S), + UPPER_T => Some(Self::T), + UPPER_U => Some(Self::U), + UPPER_V => Some(Self::V), + UPPER_W => Some(Self::W), + UPPER_X => Some(Self::X), + UPPER_Y => Some(Self::Y), + UPPER_Z => Some(Self::Z), + LOWER_A => Some(Self::a), + LOWER_B => Some(Self::b), + LOWER_C => Some(Self::c), + LOWER_D => Some(Self::d), + LOWER_E => Some(Self::e), + LOWER_F => Some(Self::f), + LOWER_G => Some(Self::g), + LOWER_H => Some(Self::h), + LOWER_I => Some(Self::i), + LOWER_J => Some(Self::j), + LOWER_K => Some(Self::k), + LOWER_L => Some(Self::l), + LOWER_M => Some(Self::m), + LOWER_N => Some(Self::n), + LOWER_O => Some(Self::o), + LOWER_P => Some(Self::p), + LOWER_Q => Some(Self::q), + LOWER_R => Some(Self::r), + LOWER_S => Some(Self::s), + LOWER_T => Some(Self::t), + LOWER_U => Some(Self::u), + LOWER_V => Some(Self::v), + LOWER_W => Some(Self::w), + LOWER_X => Some(Self::x), + LOWER_Y => Some(Self::y), + LOWER_Z => Some(Self::z), + ZERO => Some(Self::Zero), + ONE => Some(Self::One), + TWO => Some(Self::Two), + THREE => Some(Self::Three), + FOUR => Some(Self::Four), + FIVE => Some(Self::Five), + SIX => Some(Self::Six), + SEVEN => Some(Self::Seven), + EIGHT => Some(Self::Eight), + NINE => Some(Self::Nine), + HYPHEN => Some(Self::Hyphen), + UNDERSCORE => Some(Self::Underscore), + _ => None, + } + } + /// Same as [`Self::to_ascii`] except a `char` is returned. + /// + /// # Examples + /// + /// ``` + /// # use base64url_nopad::Alphabet; + /// assert_eq!(Alphabet::J.to_char(), 'J'); + /// ``` + #[inline] + #[must_use] + pub const fn to_char(self) -> char { + match self { + Self::A => UPPER_A_CHAR, + Self::B => UPPER_B_CHAR, + Self::C => UPPER_C_CHAR, + Self::D => UPPER_D_CHAR, + Self::E => UPPER_E_CHAR, + Self::F => UPPER_F_CHAR, + Self::G => UPPER_G_CHAR, + Self::H => UPPER_H_CHAR, + Self::I => UPPER_I_CHAR, + Self::J => UPPER_J_CHAR, + Self::K => UPPER_K_CHAR, + Self::L => UPPER_L_CHAR, + Self::M => UPPER_M_CHAR, + Self::N => UPPER_N_CHAR, + Self::O => UPPER_O_CHAR, + Self::P => UPPER_P_CHAR, + Self::Q => UPPER_Q_CHAR, + Self::R => UPPER_R_CHAR, + Self::S => UPPER_S_CHAR, + Self::T => UPPER_T_CHAR, + Self::U => UPPER_U_CHAR, + Self::V => UPPER_V_CHAR, + Self::W => UPPER_W_CHAR, + Self::X => UPPER_X_CHAR, + Self::Y => UPPER_Y_CHAR, + Self::Z => UPPER_Z_CHAR, + Self::a => LOWER_A_CHAR, + Self::b => LOWER_B_CHAR, + Self::c => LOWER_C_CHAR, + Self::d => LOWER_D_CHAR, + Self::e => LOWER_E_CHAR, + Self::f => LOWER_F_CHAR, + Self::g => LOWER_G_CHAR, + Self::h => LOWER_H_CHAR, + Self::i => LOWER_I_CHAR, + Self::j => LOWER_J_CHAR, + Self::k => LOWER_K_CHAR, + Self::l => LOWER_L_CHAR, + Self::m => LOWER_M_CHAR, + Self::n => LOWER_N_CHAR, + Self::o => LOWER_O_CHAR, + Self::p => LOWER_P_CHAR, + Self::q => LOWER_Q_CHAR, + Self::r => LOWER_R_CHAR, + Self::s => LOWER_S_CHAR, + Self::t => LOWER_T_CHAR, + Self::u => LOWER_U_CHAR, + Self::v => LOWER_V_CHAR, + Self::w => LOWER_W_CHAR, + Self::x => LOWER_X_CHAR, + Self::y => LOWER_Y_CHAR, + Self::z => LOWER_Z_CHAR, + Self::Zero => ZERO_CHAR, + Self::One => ONE_CHAR, + Self::Two => TWO_CHAR, + Self::Three => THREE_CHAR, + Self::Four => FOUR_CHAR, + Self::Five => FIVE_CHAR, + Self::Six => SIX_CHAR, + Self::Seven => SEVEN_CHAR, + Self::Eight => EIGHT_CHAR, + Self::Nine => NINE_CHAR, + Self::Hyphen => HYPHEN_CHAR, + Self::Underscore => UNDERSCORE_CHAR, + } + } + /// Same as [`Self::from_ascii`] except the input is a `char`. + /// + /// # Examples + /// + /// ``` + /// # use base64url_nopad::Alphabet; + /// for i in char::MIN..=char::MAX { + /// if i.is_ascii_alphanumeric() || i == '-' || i == '_' { + /// assert!(Alphabet::from_char(i).is_some()); + /// } else { + /// assert!(Alphabet::from_char(i).is_none()); + /// } + /// } + /// ``` + #[inline] + #[must_use] + pub const fn from_char(c: char) -> Option<Self> { + match c { + UPPER_A_CHAR => Some(Self::A), + UPPER_B_CHAR => Some(Self::B), + UPPER_C_CHAR => Some(Self::C), + UPPER_D_CHAR => Some(Self::D), + UPPER_E_CHAR => Some(Self::E), + UPPER_F_CHAR => Some(Self::F), + UPPER_G_CHAR => Some(Self::G), + UPPER_H_CHAR => Some(Self::H), + UPPER_I_CHAR => Some(Self::I), + UPPER_J_CHAR => Some(Self::J), + UPPER_K_CHAR => Some(Self::K), + UPPER_L_CHAR => Some(Self::L), + UPPER_M_CHAR => Some(Self::M), + UPPER_N_CHAR => Some(Self::N), + UPPER_O_CHAR => Some(Self::O), + UPPER_P_CHAR => Some(Self::P), + UPPER_Q_CHAR => Some(Self::Q), + UPPER_R_CHAR => Some(Self::R), + UPPER_S_CHAR => Some(Self::S), + UPPER_T_CHAR => Some(Self::T), + UPPER_U_CHAR => Some(Self::U), + UPPER_V_CHAR => Some(Self::V), + UPPER_W_CHAR => Some(Self::W), + UPPER_X_CHAR => Some(Self::X), + UPPER_Y_CHAR => Some(Self::Y), + UPPER_Z_CHAR => Some(Self::Z), + LOWER_A_CHAR => Some(Self::a), + LOWER_B_CHAR => Some(Self::b), + LOWER_C_CHAR => Some(Self::c), + LOWER_D_CHAR => Some(Self::d), + LOWER_E_CHAR => Some(Self::e), + LOWER_F_CHAR => Some(Self::f), + LOWER_G_CHAR => Some(Self::g), + LOWER_H_CHAR => Some(Self::h), + LOWER_I_CHAR => Some(Self::i), + LOWER_J_CHAR => Some(Self::j), + LOWER_K_CHAR => Some(Self::k), + LOWER_L_CHAR => Some(Self::l), + LOWER_M_CHAR => Some(Self::m), + LOWER_N_CHAR => Some(Self::n), + LOWER_O_CHAR => Some(Self::o), + LOWER_P_CHAR => Some(Self::p), + LOWER_Q_CHAR => Some(Self::q), + LOWER_R_CHAR => Some(Self::r), + LOWER_S_CHAR => Some(Self::s), + LOWER_T_CHAR => Some(Self::t), + LOWER_U_CHAR => Some(Self::u), + LOWER_V_CHAR => Some(Self::v), + LOWER_W_CHAR => Some(Self::w), + LOWER_X_CHAR => Some(Self::x), + LOWER_Y_CHAR => Some(Self::y), + LOWER_Z_CHAR => Some(Self::z), + ZERO_CHAR => Some(Self::Zero), + ONE_CHAR => Some(Self::One), + TWO_CHAR => Some(Self::Two), + THREE_CHAR => Some(Self::Three), + FOUR_CHAR => Some(Self::Four), + FIVE_CHAR => Some(Self::Five), + SIX_CHAR => Some(Self::Six), + SEVEN_CHAR => Some(Self::Seven), + EIGHT_CHAR => Some(Self::Eight), + NINE_CHAR => Some(Self::Nine), + HYPHEN_CHAR => Some(Self::Hyphen), + UNDERSCORE_CHAR => Some(Self::Underscore), + _ => None, + } + } +} +impl Display for Alphabet { + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_char(self.to_char()) + } +} +impl From<Alphabet> for u8 { + #[inline] + fn from(value: Alphabet) -> Self { + value.to_u8() + } +} +impl From<Alphabet> for u16 { + #[inline] + fn from(value: Alphabet) -> Self { + Self::from(value.to_u8()) + } +} +impl From<Alphabet> for u32 { + #[inline] + fn from(value: Alphabet) -> Self { + Self::from(value.to_u8()) + } +} +impl From<Alphabet> for u64 { + #[inline] + fn from(value: Alphabet) -> Self { + Self::from(value.to_u8()) + } +} +impl From<Alphabet> for u128 { + #[inline] + fn from(value: Alphabet) -> Self { + Self::from(value.to_u8()) + } +} +impl From<Alphabet> for char { + #[inline] + fn from(value: Alphabet) -> Self { + value.to_char() + } +} +/// Ordinal numbers from first to third inclusively. +enum ThreeOrdinal { + /// First. + First, + /// Second. + Second, + /// Third. + Third, +} +/// The maximum value [`encode_len_checked`] will accept before returning `None`. +// This won't `panic` since `usize::MAX` ≢ 1 (mod 4). +pub const MAX_ENCODE_INPUT_LEN: usize = decode_len(usize::MAX).unwrap(); +/// Returns the exact number of bytes needed to encode an input of length `input_length`. +/// +/// `Some` is returned iff the length needed does not exceed [`usize::MAX`]. +/// +/// Note since Rust guarantees all memory allocations don't exceed [`isize::MAX`] bytes, then one can +/// instead call [`encode_len`] when the argument passed corresponds to the length of an allocation since +/// `isize::MAX <` [`MAX_ENCODE_INPUT_LEN`]. +/// +/// # Examples +/// +/// ``` +/// # use base64url_nopad::MAX_ENCODE_INPUT_LEN; +/// assert!(base64url_nopad::encode_len_checked(usize::MAX).is_none()); +/// assert!(base64url_nopad::encode_len_checked(MAX_ENCODE_INPUT_LEN + 1).is_none()); +/// assert!(base64url_nopad::encode_len_checked(MAX_ENCODE_INPUT_LEN).is_some_and(|len| len == usize::MAX)); +/// assert!(base64url_nopad::encode_len_checked(3).is_some_and(|len| len == 4)); +/// assert!(base64url_nopad::encode_len_checked(2).is_some_and(|len| len == 3)); +/// assert!(base64url_nopad::encode_len_checked(1).is_some_and(|len| len == 2)); +/// assert!(base64url_nopad::encode_len_checked(0).is_some_and(|len| len == 0)); +/// ``` +#[expect( + clippy::arithmetic_side_effects, + clippy::integer_division, + clippy::integer_division_remainder_used, + reason = "proof and comment justifies their correctness" +)] +#[inline] +#[must_use] +pub const fn encode_len_checked(input_length: usize) -> Option<usize> { + // 256^n is the number of distinct values of the input. Let the base64 encoding in a URL safe + // way without padding of the input be O. There are 64 possible values each byte in O can be; thus we must find + // the minimum nonnegative integer m such that: + // 64^m = (2^6)^m = 2^(6m) >= 256^n = (2^8)^n = 2^(8n) + // <==> + // lg(2^(6m)) = 6m >= lg(2^(8n)) = 8n lg is defined on all positive reals which 2^(6m) and 2^(8n) are + // <==> + // m >= 8n/6 = 4n/3 + // Clearly that corresponds to m = ⌈4n/3⌉. + // We claim ⌈4n/3⌉ = 4⌊n/3⌋ + ⌈4(n mod 3)/3⌉. + // Proof: + // There are three partitions for n: + // (1) 3i = n ≡ 0 (mod 3) for some integer i + // <==> + // ⌈4n/3⌉ = ⌈4(3i)/3⌉ = ⌈4i⌉ = 4i = 4⌊i⌋ = 4⌊3i/3⌋ = 4⌊n/3⌋ + 0 = 4⌊n/3⌋ + ⌈4(0)/3⌉ = 4⌊n/3⌋ + ⌈4(n mod 3)/3⌉ + // (2) 3i + 1 = n ≡ 1 (mod 3) for some integer i + // <==> + // ⌈4n/3⌉ = ⌈4(3i + 1)/3⌉ = ⌈4i + 4/3⌉ = 4i + ⌈4/3⌉ = 4i + 2 = 4⌊i + 1/3⌋ + ⌈4(1)/3⌉ + // = 4⌊(3i + 1)/3⌋ + ⌈4((3i + 1) mod 3)/3⌉ + // = 4⌊n/3⌋ + ⌈4(n mod 3)/3⌉ + // (3) 3i + 2 = n ≡ 2 (mod 3) for some integer i + // <==> + // ⌈4n/3⌉ = ⌈4(3i + 2)/3⌉ = ⌈4i + 8/3⌉ = 4i + ⌈8/3⌉ = 4i + 3 = 4⌊i + 2/3⌋ + ⌈4(2)/3⌉ + // = 4⌊(3i + 2)/3⌋ + ⌈4((3i + 2) mod 3)/3⌉ + // = 4⌊n/3⌋ + ⌈4(n mod 3)/3⌉ + // QED + // Proof of no overflow: + // usize::MAX >= u16::MAX + // usize::MAX = 2^i - 1 where i is any integer >= 16 (due to above) + // Suppose n < 3 * 2^(i-2), then: + // ⌈4n/3⌉ < ⌈4*(3*2^(i-2))/3⌉ = ⌈2^i⌉ + // = 2^i + // = usize::MAX + 1 + // thus ignoring intermediate calcuations, the maximum possible value is usize::MAX thus overflow is not + // possible. + // QED + // Naively implementing ⌈4n/3⌉ as (4 * n).div_ceil(3) can cause overflow due to `4 * n`; thus + // we implement the equivalent equation 4⌊n/3⌋ + ⌈4(n mod 3)/3⌉ instead: + // `(4 * (n / 3)) + (4 * (n % 3)).div_ceil(3)` since none of the intermediate calculations suffer + // from overflow. + // `MAX_ENCODE_INPUT_LEN` = 3 * 2^(i-2) - 1. + if input_length <= MAX_ENCODE_INPUT_LEN { + // (n / 3) << 2u8 <= m <= usize::MAX; thus the left operand of + is fine. + // n % 3 <= 2 + // <==> + // 4(n % 3) <= 8 < usize::MAX; thus (n % 3) << 2u8 is fine. + // <==> + // ⌈4(n % 3)/3⌉ <= 4(n % 3), so the right operand of + is fine. + // The sum is fine since + // m = ⌈4n/3⌉ = 4⌊n/3⌋ + ⌈4(n mod 3)/3⌉ = ((n / 3) << 2u8) + ((n % 3) << 2u8).div_ceil(3), and m <= usize::MAX. + Some(((input_length / 3) << 2u8) + ((input_length % 3) << 2u8).div_ceil(3)) + } else { + None + } +} +/// Same as [`encode_len_checked`] except a `panic` occurs instead of `None` being returned. +/// +/// One should prefer this function over `encode_len_checked` when passing the length of a memory allocation +/// since such a length is guaranteed to succeed. +/// +/// # Panics +/// +/// `panic`s iff [`encode_len_checked`] returns `None`. +/// +/// # Examples +/// +/// ``` +/// # use base64url_nopad::MAX_ENCODE_INPUT_LEN; +/// // Uncommenting below will cause a `panic`. +/// // base64url_nopad::encode_len(usize::MAX - 4); +/// // Uncommenting below will cause a `panic`. +/// // base64url_nopad::encode_len(MAX_ENCODE_INPUT_LEN + 1); +/// assert_eq!(base64url_nopad::encode_len(MAX_ENCODE_INPUT_LEN), usize::MAX); +/// assert!(base64url_nopad::encode_len(isize::MAX as usize) > isize::MAX as usize); +/// assert_eq!(base64url_nopad::encode_len(3), 4); +/// assert_eq!(base64url_nopad::encode_len(2), 3); +/// assert_eq!(base64url_nopad::encode_len(1), 2); +/// assert_eq!(base64url_nopad::encode_len(0), 0); +/// ``` +#[expect(clippy::unwrap_used, reason = "comment justifies correctness")] +#[inline] +#[must_use] +pub const fn encode_len(input_length: usize) -> usize { + // A precondition for calling this function is to ensure `encode_len_checked` can't return `None`. + encode_len_checked(input_length).unwrap() +} +/// Encodes `input` into `output` re-interpreting the encoded subset of `output` as a `str` before returning it. +/// +/// `Some` is returned iff `output.len()` is large enough to write the encoded data into. +/// +/// Note since Rust guarantees all memory allocations don't exceed [`isize::MAX`] bytes, one can +/// instead call [`encode_buffer`] using a buffer whose length is at least as large as the value returned from +/// [`encode_len`] without fear of a `panic` and the benefit of getting a `str` instead of an `Option`. +/// +/// # Examples +/// +/// ``` +/// assert!( +/// base64url_nopad::encode_buffer_checked([0; 0].as_slice(), [0; 0].as_mut_slice()).is_some_and(|val| val.is_empty()) +/// ); +/// assert!( +/// base64url_nopad::encode_buffer_checked([0; 1].as_slice(), [0; 2].as_mut_slice()).is_some_and(|val| val == "AA") +/// ); +/// // A larger output buffer than necessary is OK. +/// assert!( +/// base64url_nopad::encode_buffer_checked([1; 1].as_slice(), [0; 128].as_mut_slice()).is_some_and(|val| val == "AQ") +/// ); +/// assert!( +/// base64url_nopad::encode_buffer_checked( +/// [0xc9; 14].as_slice(), +/// [0; base64url_nopad::encode_len(14)].as_mut_slice() +/// ).is_some_and(|val| val == "ycnJycnJycnJycnJyck") +/// ); +/// assert!(base64url_nopad::encode_buffer_checked([0; 1].as_slice(), [0; 1].as_mut_slice()).is_none()); +/// ``` +#[expect(unsafe_code, reason = "comments justify correctness")] +#[expect( + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + reason = "comments justify correctness" +)] +#[inline] +pub const fn encode_buffer_checked<'a>( + mut input: &[u8], + output: &'a mut [u8], +) -> Option<&'a mut str> { + // This won't `panic` since Rust guarantees that all memory allocations won't exceed `isize::MAX`. + let final_len = encode_len(input.len()); + if output.len() >= final_len { + // We increment this by `1` for each `u8` in `input`. On every third `u8`, we increment it an extra + // time since we use 4 base64url `u8`s for each `u8`. + // We also verified that `output.len()` large enough; thus all indexing operations + // using it are correct, and incrementing it never results in overflow. + let mut output_idx = 0; + let mut counter = ThreeOrdinal::First; + let mut trailing = 0; + let mut shift; + while let [first, ref rest @ ..] = *input { + match counter { + ThreeOrdinal::First => { + // We trim the last two bits and interpret `first` as a 6-bit integer. + shift = first >> 2; + // SAFETY: + // `shift <= 63` since we shifted at least two bits to the right. + output[output_idx] = unsafe { Alphabet::from_u8_unchecked(shift) }.to_ascii(); + // The two bits we trimmed are the first two bits of the next 6-bit integer. + trailing = (first & 3) << 4; + counter = ThreeOrdinal::Second; + } + ThreeOrdinal::Second => { + // We trim the last four bits and interpret `first` as a 6-bit integer. + // The first two bits are the trailing 2 bits from the previous value. + shift = trailing | (first >> 4); + // SAFETY: + // `shift <= 63` since `first` was shifted at least two bits to the right, and + // `trailing = (first & 3) << 4` which means its high two bits are 0 as well. + output[output_idx] = unsafe { Alphabet::from_u8_unchecked(shift) }.to_ascii(); + // The four bits we trimmed are the first four bits of the next 6-bit integer. + trailing = (first & 15) << 2; + counter = ThreeOrdinal::Third; + } + ThreeOrdinal::Third => { + // We trim the last six bits and interpret `first` as a 6-bit integer. + // The first four bits are the trailing 4 bits from the previous value. + shift = trailing | (first >> 6); + // SAFETY: + // `shift <= 63` since `first` was shifted at least two bits to the right, and + // `trailing = (first & 15) << 2` which means its high two bits are 0 as well. + output[output_idx] = unsafe { Alphabet::from_u8_unchecked(shift) }.to_ascii(); + // Every third `u8` corresponds to a fourth base64url `u8`. + output_idx += 1; + // We use the 6 bits we just trimmed. + shift = first & 63; + // SAFETY: + // `shift <= 63` since `first & 63` is. + output[output_idx] = unsafe { Alphabet::from_u8_unchecked(shift) }.to_ascii(); + counter = ThreeOrdinal::First; + } + } + input = rest; + output_idx += 1; + } + if !matches!(counter, ThreeOrdinal::First) { + // `input.len()` is not a multiple of 3; thus we have to append a trailing base64url `u8` that + // is simply the current value of `trailing`. + // SAFETY: + // `trailing <= 63` since `trailing` is either `(first & 3) << 4` or `(first & 15) << 2` where + // `first` is any `u8`. This means the high two bits are guaranteed to be 0. + output[output_idx] = unsafe { Alphabet::from_u8_unchecked(trailing) }.to_ascii(); + } + // SAFETY: + // We verified `output.len() >= final_len`. + let val = unsafe { output.split_at_mut_unchecked(final_len) }.0; + // SAFETY: + // `val` has the exact length needed to encode `input`, and all of the `u8`s in it + // are from `Alphabet::to_ascii` which is a subset of UTF-8; thus this is safe. + // Note the above is vacuously true when `val` is empty. + Some(unsafe { str::from_utf8_unchecked_mut(val) }) + } else { + None + } +} +/// Same as [`encode_buffer_checked`] except a `panic` occurs instead of `None` being returned. +/// +/// # Panics +/// +/// `panic`s iff [`encode_buffer_checked`] returns `None` (i.e., the length of the output buffer is too small). +/// +/// # Examples +/// +/// ``` +/// assert_eq!( +/// base64url_nopad::encode_buffer([0; 0].as_slice(), [0; 0].as_mut_slice()), +/// "" +/// ); +/// assert_eq!( +/// base64url_nopad::encode_buffer([0; 1].as_slice(), [0; 2].as_mut_slice()), +/// "AA" +/// ); +/// // A larger output buffer than necessary is OK. +/// assert_eq!( +/// base64url_nopad::encode_buffer([255; 1].as_slice(), [0; 256].as_mut_slice()), +/// "_w" +/// ); +/// assert_eq!( +/// base64url_nopad::encode_buffer( +/// [0xc9; 14].as_slice(), +/// [0; base64url_nopad::encode_len(14)].as_mut_slice() +/// ), +/// "ycnJycnJycnJycnJyck" +/// ); +/// // The below will `panic` when uncommented since the supplied output buffer is too small. +/// // _ = base64url_nopad::encode_buffer([0; 1].as_slice(), [0; 1].as_mut_slice()); +/// ``` +#[expect(clippy::unwrap_used, reason = "comment justifies correctness")] +#[inline] +pub const fn encode_buffer<'a>(input: &[u8], output: &'a mut [u8]) -> &'a mut str { + // A precondition for calling this function is to ensure `encode_buffer_checked` can't return `None`. + encode_buffer_checked(input, output).unwrap() +} +/// Similar to [`encode_buffer`] except a `String` is returned instead using its buffer to write to. +/// +/// # Errors +/// +/// Errors iff an error occurs from allocating the capacity needed to contain the encoded data. +/// +/// # Examples +/// +/// ``` +/// # extern crate alloc; +/// # use alloc::collections::TryReserveError; +/// assert_eq!( +/// base64url_nopad::try_encode([0; 0].as_slice())?, +/// "" +/// ); +/// assert_eq!( +/// base64url_nopad::try_encode([0; 1].as_slice())?, +/// "AA" +/// ); +/// assert_eq!( +/// base64url_nopad::try_encode([128, 40, 3].as_slice())?, +/// "gCgD" +/// ); +/// assert_eq!( +/// base64url_nopad::try_encode([0x7b; 22].as_slice())?, +/// "e3t7e3t7e3t7e3t7e3t7e3t7e3t7ew" +/// ); +/// # Ok::<_, TryReserveError>(()) +/// ``` +#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] +#[cfg(feature = "alloc")] +#[expect(unsafe_code, reason = "comment justifies correctness")] +#[inline] +pub fn try_encode(input: &[u8]) -> Result<String, TryReserveError> { + let mut output = Vec::new(); + // `encode_len` won't `panic` since Rust guarantees `input.len()` will not return a value greater + // than `isize::MAX`. + let len = encode_len(input.len()); + output.try_reserve_exact(len).map(|()| { + output.resize(len, 0); + _ = encode_buffer(input, output.as_mut_slice()); + // SAFETY: + // `output` has the exact length needed to encode `input`, and all of the `u8`s in it + // are from `Alphabet` which is a subset of UTF-8; thus this is safe. + // Note the above is vacuously true when `output` is empty. + unsafe { String::from_utf8_unchecked(output) } + }) +} +/// Same as [`try_encode`] except a `panic` occurs on allocation failure. +/// +/// # Panics +/// +/// `panic`s iff [`try_encode`] errors. +/// +/// # Examples +/// +/// ``` +/// assert_eq!( +/// base64url_nopad::encode([0; 0].as_slice()), +/// "" +/// ); +/// assert_eq!( +/// base64url_nopad::encode([0; 1].as_slice()), +/// "AA" +/// ); +/// assert_eq!( +/// base64url_nopad::encode([128, 40, 3].as_slice()), +/// "gCgD" +/// ); +/// assert_eq!( +/// base64url_nopad::encode([0x7b; 22].as_slice()), +/// "e3t7e3t7e3t7e3t7e3t7e3t7e3t7ew" +/// ); +/// ``` +#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] +#[cfg(feature = "alloc")] +#[expect( + clippy::unwrap_used, + reason = "purpose of function is to panic on allocation failure" +)] +#[inline] +#[must_use] +pub fn encode(input: &[u8]) -> String { + try_encode(input).unwrap() +} +/// Writes the base64url encoding of `input` using `writer`. +/// +/// Internally a buffer of at most 1024 bytes is used to write the encoded data. Smaller buffers may be used +/// for small inputs. +/// +/// # Errors +/// +/// Errors iff [`Write::write_str`] does. +/// +/// # Panics +/// +/// `panic`s iff [`Write::write_str`] does. +/// +/// # Examples +/// +/// ``` +/// # extern crate alloc; +/// # use alloc::string::String; +/// # use core::fmt::Error; +/// let mut buffer = String::new(); +/// base64url_nopad::encode_write([0; 0].as_slice(), &mut buffer)?; +/// assert_eq!(buffer, ""); +/// buffer.clear(); +/// base64url_nopad::encode_write([0; 1].as_slice(), &mut buffer)?; +/// assert_eq!(buffer, "AA"); +/// buffer.clear(); +/// base64url_nopad::encode_write( +/// [0xc9; 14].as_slice(), +/// &mut buffer, +/// )?; +/// assert_eq!(buffer, "ycnJycnJycnJycnJyck"); +/// # Ok::<_, Error>(()) +/// ``` +#[expect(unsafe_code, reason = "comment justifies correctness")] +#[expect( + clippy::arithmetic_side_effects, + clippy::as_conversions, + clippy::indexing_slicing, + reason = "comments justify correctness" +)] +#[inline] +pub fn encode_write<W: Write>(mut input: &[u8], writer: &mut W) -> fmt::Result { + /// The minimum buffer size we use. + const MIN_BUFFER_LEN: usize = 256; + /// The medium buffer size we use. + const MID_BUFFER_LEN: usize = MIN_BUFFER_LEN << 1; + /// The max buffer size. + /// + /// This must be at least 4, no more than `i16::MAX`, and must be a power of 2. + const MAX_BUFFER_LEN: usize = MID_BUFFER_LEN << 1; + /// The minimum length of the input until we must chunk encode the data. + const LOOP_LEN: usize = MAX_BUFFER_LEN + 1; + /// Want to ensure at compilation time that `MAX_BUFFER_LEN` upholds its invariants. Namely + /// that it's at least as large as 4, doesn't exceed [`i16::MAX`], and is always a power of 2. + const _: () = { + // `i16::MAX <= usize::MAX`, so this is fine. + /// `i16::MAX`. + const MAX_LEN: usize = i16::MAX as usize; + assert!( + 4 <= MAX_BUFFER_LEN && MAX_BUFFER_LEN < MAX_LEN && MAX_BUFFER_LEN.is_power_of_two(), + "encode_write::MAX_BUFFER_LEN must be a power of two less than i16::MAX but at least as large as 4" + ); + }; + /// The input size that corresponds to an encoded value of length `MAX_BUFFER_LEN`. + // This will never `panic` since `MAX_BUFFER_LEN` is a power of two at least as large as 4 + // (i.e., `MAX_BUFFER_LEN` ≢ 1 (mod 4)). + const INPUT_LEN: usize = decode_len(MAX_BUFFER_LEN).unwrap(); + // This won't `panic` since `input.len()` is guaranteed to be no more than `isize::MAX`. + let len = encode_len(input.len()); + match len { + 0 => Ok(()), + 1..=MIN_BUFFER_LEN => { + let mut buffer = [0; MIN_BUFFER_LEN]; + // `buffer.len() == MIN_BUFFER_LEN >= len`, so indexing is fine. + // `encode_buffer` won't `panic` since `len` is the exact number of bytes needed to encode + // the data. + writer.write_str(encode_buffer(input, &mut buffer[..len])) + } + 257..=MID_BUFFER_LEN => { + let mut buffer = [0; MID_BUFFER_LEN]; + // `buffer.len() == MID_BUFFER_LEN >= len`, so indexing is fine. + // `encode_buffer` won't `panic` since `len` is the exact number of bytes needed to encode + // the data. + writer.write_str(encode_buffer(input, &mut buffer[..len])) + } + 513..=MAX_BUFFER_LEN => { + let mut buffer = [0; MAX_BUFFER_LEN]; + // `buffer.len() == MAX_BUFFER_LEN >= len`, so indexing is fine. + // `encode_buffer` won't `panic` since `len` is the exact number of bytes needed to encode + // the data. + writer.write_str(encode_buffer(input, &mut buffer[..len])) + } + LOOP_LEN.. => { + let mut buffer = [0; MAX_BUFFER_LEN]; + let mut counter = 0; + // `len / MAX_BUFFER_LEN` is equal to ⌊len / MAX_BUFFER_LEN⌋ since `MAX_BUFFER_LEN` is a power of two. + // We can safely encode `term` chunks of `INPUT_LEN` length into `buffer`. + let term = len >> MAX_BUFFER_LEN.trailing_zeros(); + let mut input_buffer; + while counter < term { + // SAFETY: + // `input.len() >= INPUT_LEN`. + input_buffer = unsafe { input.split_at_unchecked(INPUT_LEN) }; + // `encode_buffer` won't `panic` since `buffer` has length `MAX_BUFFER_LEN` which + // is the exact length needed for `INPUT_LEN` length inputs which `input_buffer.0` is. + writer.write_str(encode_buffer(input_buffer.0, buffer.as_mut_slice()))?; + input = input_buffer.1; + // `counter < term`, so overflow cannot happen. + counter += 1; + } + // `encode_len` won't `panic` since `input.len() < MAX_ENCODE_INPUT_LEN`. + // `input.len() < INPUT_LEN`; thus `encode_len(input.len()) < MAX_BUFFER_LEN = buffer.len()` so + // indexing is fine. + // `encode_buffer` won't `panic` since the buffer is the exact length needed to encode `input`. + writer.write_str(encode_buffer(input, &mut buffer[..encode_len(input.len())])) + } + } +} +/// Appends the base64url encoding of `input` to `s` returning the `str` that was appended. +/// +/// # Errors +/// +/// Errors iff an error occurs from allocating the capacity needed to append the encoded data. +/// +/// # Examples +/// +/// ``` +/// # extern crate alloc; +/// # use alloc::{collections::TryReserveError, string::String}; +/// let mut buffer = String::new(); +/// assert_eq!( +/// base64url_nopad::try_encode_append([0; 0].as_slice(), &mut buffer)?, +/// "" +/// ); +/// assert_eq!( +/// base64url_nopad::try_encode_append([0; 1].as_slice(), &mut buffer)?, +/// "AA" +/// ); +/// assert_eq!( +/// base64url_nopad::try_encode_append([128, 40, 3].as_slice(), &mut buffer)?, +/// "gCgD" +/// ); +/// assert_eq!(buffer, "AAgCgD"); +/// assert_eq!( +/// base64url_nopad::try_encode_append([0x7b; 22].as_slice(), &mut buffer)?, +/// "e3t7e3t7e3t7e3t7e3t7e3t7e3t7ew" +/// ); +/// assert_eq!(buffer, "AAgCgDe3t7e3t7e3t7e3t7e3t7e3t7e3t7ew"); +/// # Ok::<_, TryReserveError>(()) +/// ``` +#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] +#[cfg(feature = "alloc")] +#[expect(unsafe_code, reason = "comment justifies correctness")] +#[expect( + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + reason = "comments justify correctness" +)] +#[inline] +pub fn try_encode_append<'a>( + input: &[u8], + s: &'a mut String, +) -> Result<&'a mut str, TryReserveError> { + // `encode_len` won't `panic` since Rust guarantees `input.len()` will return a value no larger + // than `isize::MAX`. + let additional_len = encode_len(input.len()); + s.try_reserve_exact(additional_len).map(|()| { + // SAFETY: + // We only append base64url ASCII which is a subset of UTF-8, so this will remain valid UTF-8. + let utf8 = unsafe { s.as_mut_vec() }; + let original_len = utf8.len(); + // Overflow can't happen; otherwise `s.try_reserve_exact` would have erred. + utf8.resize(original_len + additional_len, 0); + // `utf8.len() >= original_len`, so indexing is fine. + // `encode_buffer` won't `panic` since `utf8[original_len..]` has length `additional_len` + // which is the exact number of bytes needed to encode `input`. + encode_buffer(input, &mut utf8[original_len..]) + }) +} +/// Same as [`try_encode_append`] except the encoded `str` is not returned. +/// +/// # Errors +/// +/// Errors iff [`try_encode_append`] does. +/// +/// # Examples +/// +/// ``` +/// # extern crate alloc; +/// # use alloc::{collections::TryReserveError, string::String}; +/// let mut buffer = String::new(); +/// base64url_nopad::try_encode_append_only([0; 0].as_slice(), &mut buffer)?; +/// assert_eq!(buffer, ""); +/// base64url_nopad::try_encode_append_only([0; 1].as_slice(), &mut buffer)?; +/// assert_eq!(buffer, "AA"); +/// base64url_nopad::try_encode_append_only([128, 40, 3].as_slice(), &mut buffer)?; +/// assert_eq!(buffer, "AAgCgD"); +/// base64url_nopad::try_encode_append_only([0x7b; 22].as_slice(), &mut buffer)?; +/// assert_eq!(buffer, "AAgCgDe3t7e3t7e3t7e3t7e3t7e3t7e3t7ew"); +/// # Ok::<_, TryReserveError>(()) +/// ``` +#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] +#[cfg(feature = "alloc")] +#[inline] +pub fn try_encode_append_only(input: &[u8], s: &mut String) -> Result<(), TryReserveError> { + try_encode_append(input, s).map(|_| ()) +} +/// Same as [`try_encode_append`] except a `panic` occurs on allocation failure. +/// +/// # Panics +/// +/// `panic`s iff [`try_encode_append`] errors. +/// +/// # Examples +/// +/// ``` +/// # extern crate alloc; +/// # use alloc::{collections::TryReserveError, string::String}; +/// let mut buffer = String::new(); +/// assert_eq!( +/// base64url_nopad::encode_append([0; 0].as_slice(), &mut buffer), +/// "" +/// ); +/// assert_eq!( +/// base64url_nopad::encode_append([0; 1].as_slice(), &mut buffer), +/// "AA" +/// ); +/// assert_eq!( +/// base64url_nopad::encode_append([128, 40, 3].as_slice(), &mut buffer), +/// "gCgD" +/// ); +/// assert_eq!(buffer, "AAgCgD"); +/// assert_eq!( +/// base64url_nopad::encode_append([0x7b; 22].as_slice(), &mut buffer), +/// "e3t7e3t7e3t7e3t7e3t7e3t7e3t7ew" +/// ); +/// assert_eq!(buffer, "AAgCgDe3t7e3t7e3t7e3t7e3t7e3t7e3t7ew"); +/// ``` +#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] +#[cfg(feature = "alloc")] +#[expect( + clippy::unwrap_used, + reason = "purpose of this function is to panic on allocation failure" +)] +#[inline] +pub fn encode_append<'a>(input: &[u8], s: &'a mut String) -> &'a mut str { + try_encode_append(input, s).unwrap() +} +/// Same as [`encode_append`] except the encoded `str` is not returned. +/// +/// # Panics +/// +/// `panic`s iff [`encode_append`] does. +/// +/// # Examples +/// +/// ``` +/// # extern crate alloc; +/// # use alloc::{collections::TryReserveError, string::String}; +/// let mut buffer = String::new(); +/// base64url_nopad::encode_append_only([0; 0].as_slice(), &mut buffer); +/// assert_eq!(buffer, ""); +/// base64url_nopad::encode_append_only([0; 1].as_slice(), &mut buffer); +/// assert_eq!(buffer, "AA"); +/// base64url_nopad::encode_append_only([128, 40, 3].as_slice(), &mut buffer); +/// assert_eq!(buffer, "AAgCgD"); +/// base64url_nopad::encode_append_only([0x7b; 22].as_slice(), &mut buffer); +/// assert_eq!(buffer, "AAgCgDe3t7e3t7e3t7e3t7e3t7e3t7e3t7ew"); +/// # Ok::<_, TryReserveError>(()) +/// ``` +#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] +#[cfg(feature = "alloc")] +#[inline] +pub fn encode_append_only(input: &[u8], s: &mut String) { + _ = encode_append(input, s); +} +/// Returns the exact number of bytes needed to decode a base64url without padding input of length `input_length`. +/// +/// `Some` is returned iff `input_length` represents a possible length of a base64url without padding input. +/// +/// # Examples +/// +/// ``` +/// # use base64url_nopad::MAX_ENCODE_INPUT_LEN; +/// assert!(base64url_nopad::decode_len(1).is_none()); +/// assert!(base64url_nopad::decode_len(usize::MAX).is_some_and(|len| len == MAX_ENCODE_INPUT_LEN)); +/// assert!(base64url_nopad::decode_len(4).is_some_and(|len| len == 3)); +/// assert!(base64url_nopad::decode_len(3).is_some_and(|len| len == 2)); +/// assert!(base64url_nopad::decode_len(2).is_some_and(|len| len == 1)); +/// assert!(base64url_nopad::decode_len(0).is_some_and(|len| len == 0)); +/// ``` +#[expect( + clippy::arithmetic_side_effects, + reason = "proof and comment justifies their correctness" +)] +#[inline] +#[must_use] +pub const fn decode_len(input_length: usize) -> Option<usize> { + // 64^n is the number of distinct values of the input. Let the decoded output be O. + // There are 256 possible values each byte in O can be; thus we must find + // the maximum nonnegative integer m such that: + // 256^m = (2^8)^m = 2^(8m) <= 64^n = (2^6)^n = 2^(6n) + // <==> + // lg(2^(8m)) = 8m <= lg(2^(6n)) = 6n lg is defined on all positive reals which 2^(8m) and 2^(6n) are + // <==> + // m <= 6n/8 = 3n/4 + // Clearly that corresponds to m = ⌊3n/4⌋. + // From the proof in `encode_len_checked`, we know that n is a valid length + // iff n ≢ 1 (mod 4). + // We claim ⌊3n/4⌋ = 3⌊n/4⌋ + ⌊3(n mod 4)/4⌋. + // Proof: + // There are three partitions for n: + // (1) 4i = n ≡ 0 (mod 4) for some integer i + // <==> + // ⌊3n/4⌋ = ⌊3(4i)/4⌋ = ⌊3i⌋ = 3i = 3⌊i⌋ = 3⌊4i/4⌋ = 3⌊n/4⌋ + 0 = 3⌊n/4⌋ + ⌊3(0)/4⌋ = 3⌊n/4⌋ + ⌊3(n mod 4)/4⌋ + // (2) 4i + 2 = n ≡ 2 (mod 4) for some integer i + // <==> + // ⌊3n/4⌋ = ⌊3(4i + 2)/4⌋ = ⌊3i + 6/4⌋ = 3i + ⌊6/4⌋ = 3i + 1 = 3⌊i⌋ + ⌊3(2)/4⌋ + // = 3⌊(4i + 2)/4⌋ + ⌊3((4i + 2) mod 4)/4⌋ + // = 3⌊n/4⌋ + ⌊3(n mod 4)/4⌋ + // (3) 4i + 3 = n ≡ 3 (mod 4) for some integer i + // <==> + // ⌊3n/4⌋ = ⌊3(4i + 3)/4⌋ = ⌊3i + 9/4⌋ = 3i + ⌊9/4⌋ = 3i + 2 = 3⌊i⌋ + ⌊3(3)/4⌋ + // = 3⌊(4i + 3)/4⌋ + ⌊3((4i + 3) mod 4)/4⌋ + // = 3⌊n/4⌋ + ⌊3(n mod 4)/4⌋ + // QED + // Naively implementing ⌊3n/4⌋ as (3 * n) / 4 can cause overflow due to `3 * n`; thus + // we implement the equivalent equation 3⌊n/4⌋ + ⌊3(n mod 4)/4⌋ instead: + // `(3 * (n / 4)) + ((3 * (n % 4)) / 4)` since none of the intermediate calculations suffer + // from overflow. + // `input_length % 4`. + let rem = input_length & 3; + if rem == 1 { + None + } else { + // 3 * (n >> 2u8) <= m < usize::MAX; thus the left operand of + is fine. + // rem <= 3 + // <==> + // 3rem <= 9 < usize::MAX; thus 3 * rem is fine. + // <==> + // ⌊3rem/4⌋ <= 3rem, so the right operand of + is fine. + // The sum is fine since + // m = ⌊3n/4⌋ = 3⌊n/4⌋ + ⌊3(n mod 4)/4⌋ = (3 * (n >> 2u8)) + ((3 * rem) >> 2u8), and m < usize::MAX. + Some((3 * (input_length >> 2u8)) + ((3 * rem) >> 2u8)) + } +} +/// Ordinal numbers from first to fourth inclusively. +enum FourOrdinal { + /// First. + First, + /// Second. + Second, + /// Third. + Third, + /// Fourth. + Fourth, +} +/// Error returned from [`decode_buffer`] and [`decode`]. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum DecodeErr { + /// The encoded input had an invalid length. + EncodedLen, + /// The buffer supplied had a length that was too small to contain the decoded data. + BufferLen, + /// The encoded data contained trailing bits that were not zero. + TrailingBits, + /// The encoded data contained an invalid `u8`. + InvalidByte, + /// [`decode`] could not allocate enough memory to contain the decoded data. + #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] + #[cfg(feature = "alloc")] + TryReserve(TryReserveError), +} +#[cfg_attr(docsrs, doc(cfg(not(feature = "alloc"))))] +#[cfg(not(feature = "alloc"))] +impl Copy for DecodeErr {} +impl Display for DecodeErr { + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match *self { + Self::EncodedLen => f.write_str("length of encoded data was invalid"), + Self::BufferLen => { + f.write_str("length of the output buffer is too small to contain the decoded data") + } + Self::TrailingBits => { + f.write_str("encoded data contained trailing bits that were not zero") + } + Self::InvalidByte => f.write_str("encoded data contained an invalid byte"), + #[cfg(feature = "alloc")] + Self::TryReserve(ref err) => err.fmt(f), + } + } +} +impl Error for DecodeErr {} +/// Decodes `input` into `output` returning the subset of `output` containing the decoded data. +/// +/// # Errors +/// +/// Errors iff [`decode_len`] of `input.len()` does not return `Some` containing a +/// `usize` that does not exceed `ouput.len()` or `input` is an invalid base64url-encoded value without padding. +/// Note [`DecodeErr::TryReserve`] will never be returned. +/// +/// # Examples +/// +/// ``` +/// # use base64url_nopad::DecodeErr; +/// assert_eq!(base64url_nopad::decode_buffer([0; 0].as_slice(), [0; 0].as_mut_slice())?, b""); +/// assert_eq!( +/// base64url_nopad::decode_buffer([0; 1].as_slice(), [0; 0].as_mut_slice()).unwrap_err(), +/// DecodeErr::EncodedLen +/// ); +/// assert_eq!( +/// base64url_nopad::decode_buffer([0; 2].as_slice(), [0; 3].as_mut_slice()).unwrap_err(), +/// DecodeErr::InvalidByte +/// ); +/// assert_eq!( +/// base64url_nopad::decode_buffer([0; 2].as_slice(), [0; 0].as_mut_slice()).unwrap_err(), +/// DecodeErr::BufferLen +/// ); +/// assert_eq!( +/// base64url_nopad::decode_buffer(b"-8", [0; 3].as_mut_slice()).unwrap_err(), +/// DecodeErr::TrailingBits +/// ); +/// // A larger output buffer than necessary is OK. +/// assert_eq!(base64url_nopad::decode_buffer(b"C8Aa_A--91VZbx0", &mut [0; 128])?, [0x0b, 0xc0, 0x1a, 0xfc, 0x0f, 0xbe, 0xf7, b'U', b'Y', b'o', 0x1d]); +/// # Ok::<_, DecodeErr>(()) +/// ``` +#[expect(unsafe_code, reason = "comment justifies correctness")] +#[expect( + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + reason = "comments justify correctness" +)] +#[inline] +pub const fn decode_buffer<'a>( + mut input: &[u8], + output: &'a mut [u8], +) -> Result<&'a mut [u8], DecodeErr> { + if let Some(output_len) = decode_len(input.len()) { + if output.len() >= output_len { + // `input.len() % 4`. + let len = input.len() & 3; + // A trailing `Alphabet` is added iff the encode value is not a multiple of 4 (i.e., len % 4 != 0). + match len { + 2 => { + // We know `input` is not empty; otherwise `len % 3 == 0`. + if let Some(val) = Alphabet::from_ascii(input[input.len() - 1]) { + if val.to_u8().trailing_zeros() < 4 { + return Err(DecodeErr::TrailingBits); + } + } else { + return Err(DecodeErr::InvalidByte); + } + } + 3 => { + // We know `input` is not empty; otherwise `len % 3 == 0`. + if let Some(val) = Alphabet::from_ascii(input[input.len() - 1]) { + if val.to_u8().trailing_zeros() < 2 { + return Err(DecodeErr::TrailingBits); + } + } else { + return Err(DecodeErr::InvalidByte); + } + } + // The only possible value is `0` since if `len` were `1`, `decode_len` would have failed. + _ => {} + } + let mut val = 0; + let mut output_idx = 0; + let mut counter = FourOrdinal::First; + while let [mut first, ref rest @ ..] = *input { + if let Some(base64) = Alphabet::from_ascii(first) { + first = base64.to_u8(); + match counter { + FourOrdinal::First => { + val = first << 2; + counter = FourOrdinal::Second; + } + FourOrdinal::Second => { + output[output_idx] = val | (first >> 4); + val = first << 4; + counter = FourOrdinal::Third; + output_idx += 1; + } + FourOrdinal::Third => { + output[output_idx] = val | (first >> 2); + val = first << 6; + counter = FourOrdinal::Fourth; + output_idx += 1; + } + FourOrdinal::Fourth => { + output[output_idx] = val | first; + counter = FourOrdinal::First; + output_idx += 1; + } + } + input = rest; + } else { + return Err(DecodeErr::InvalidByte); + } + } + // SAFETY: + // `output.len() >= output_len`. + Ok(unsafe { output.split_at_mut_unchecked(output_len) }.0) + } else { + Err(DecodeErr::BufferLen) + } + } else { + Err(DecodeErr::EncodedLen) + } +} +/// Similar to [`decode_buffer`] except a `Vec` is returned instead using its buffer to write to. +/// +/// # Errors +/// +/// Errors iff [`decode_buffer`] errors or an error occurs from allocating the capacity needed to contain +/// the decoded data. Note [`DecodeErr::BufferLen`] is not possible to be returned. +/// +/// # Examples +/// +/// ``` +/// # use base64url_nopad::DecodeErr; +/// assert_eq!(base64url_nopad::decode([0; 0].as_slice())?, b""); +/// assert_eq!( +/// base64url_nopad::decode([0; 1].as_slice()).unwrap_err(), +/// DecodeErr::EncodedLen +/// ); +/// assert_eq!( +/// base64url_nopad::decode([0; 2].as_slice()).unwrap_err(), +/// DecodeErr::InvalidByte +/// ); +/// assert_eq!( +/// base64url_nopad::decode(b"-8").unwrap_err(), +/// DecodeErr::TrailingBits +/// ); +/// assert_eq!(base64url_nopad::decode(b"C8Aa_A--91VZbx0")?, [0x0b, 0xc0, 0x1a, 0xfc, 0x0f, 0xbe, 0xf7, b'U', b'Y', b'o', 0x1d]); +/// # Ok::<_, DecodeErr>(()) +/// ``` +#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] +#[cfg(feature = "alloc")] +#[inline] +pub fn decode(input: &[u8]) -> Result<Vec<u8>, DecodeErr> { + decode_len(input.len()) + .ok_or(DecodeErr::EncodedLen) + .and_then(|capacity| { + let mut buffer = Vec::new(); + buffer + .try_reserve_exact(capacity) + .map_err(DecodeErr::TryReserve) + .and_then(|()| { + buffer.resize(capacity, 0); + if let Err(e) = decode_buffer(input, buffer.as_mut_slice()) { + Err(e) + } else { + Ok(buffer) + } + }) + }) +} +/// Similar to [`decode_buffer`] except the data is not decoded. +/// +/// In some situations, one does not want to actually decode data but merely validate that the encoded data +/// is valid base64url without padding. Since data is not actually decoded, one avoids the need to allocate +/// a large-enough buffer first. +/// +/// # Errors +/// +/// Errors iff `input` is an invalid base64url without padding. +/// +/// Note since no buffer is used to decode the data into, neither [`DecodeErr::BufferLen`] nor +/// [`DecodeErr::TryReserve`] will ever be returned. +/// +/// # Examples +/// +/// ``` +/// # use base64url_nopad::DecodeErr; +/// base64url_nopad::validate_encoded_data([0; 0].as_slice())?; +/// assert_eq!( +/// base64url_nopad::validate_encoded_data([0; 1].as_slice()).unwrap_err(), +/// DecodeErr::EncodedLen +/// ); +/// assert_eq!( +/// base64url_nopad::validate_encoded_data([0; 2].as_slice()).unwrap_err(), +/// DecodeErr::InvalidByte +/// ); +/// assert_eq!( +/// base64url_nopad::validate_encoded_data(b"-8").unwrap_err(), +/// DecodeErr::TrailingBits +/// ); +/// base64url_nopad::validate_encoded_data(b"C8Aa_A--91VZbx0")?; +/// # Ok::<_, DecodeErr>(()) +/// ``` +#[expect( + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + reason = "comments justify correctness" +)] +#[inline] +pub const fn validate_encoded_data(mut input: &[u8]) -> Result<(), DecodeErr> { + let len = input.len(); + // `len % 4`. + match len & 3 { + // `input.len()` is invalid iff it is equivalent to 1 modulo 4 per the proof in + // `decode_len`. + 1 => return Err(DecodeErr::EncodedLen), + 2 => { + // We know `input` is not empty; otherwise `len % 4 == 0`. + if let Some(val) = Alphabet::from_ascii(input[len - 1]) { + if val.to_u8().trailing_zeros() < 4 { + return Err(DecodeErr::TrailingBits); + } + } else { + return Err(DecodeErr::InvalidByte); + } + } + 3 => { + // We know `input` is not empty; otherwise `len % 4 == 0`. + if let Some(val) = Alphabet::from_ascii(input[len - 1]) { + if val.to_u8().trailing_zeros() < 2 { + return Err(DecodeErr::TrailingBits); + } + } else { + return Err(DecodeErr::InvalidByte); + } + } + // When the input has length that is a multple of 4, then no trailing bits were added and thus + // all values are possible. + _ => {} + } + while let [first, ref rest @ ..] = *input { + if Alphabet::from_ascii(first).is_some() { + input = rest; + } else { + return Err(DecodeErr::InvalidByte); + } + } + Ok(()) +} +/// Same as [`encode_buffer`] except `output` must have the _exact_ length needed to encode `input`, and the +/// encoded `str` is not returned. +/// +/// # Panics +/// +/// `panic`s iff `output` does not have the _exact_ length needed to encode `input`. +/// +/// # Examples +/// +/// ``` +/// let mut buffer = [0; 256]; +/// base64url_nopad::encode_buffer_exact([0; 0].as_slice(), &mut buffer[..0]); +/// base64url_nopad::encode_buffer_exact([0; 1].as_slice(), &mut buffer[..2]); +/// assert_eq!(*b"AA", buffer[..2]); +/// // Uncommenting below will cause a `panic` since the output buffer must be exact. +/// // base64url_nopad::encode_buffer_exact([255; 1].as_slice(), &mut buffer); +/// ``` +#[inline] +pub const fn encode_buffer_exact(input: &[u8], output: &mut [u8]) { + assert!( + // `encode_len` won't `panic` since Rust guarantees `input.len()` is at most `isize::MAX`. + output.len() == encode_len(input.len()), + "encode_buffer_exact must be passed an output buffer whose length is exactly the length needed to encode the data" + ); + _ = encode_buffer(input, output); +} +/// Same as [`decode_buffer`] except `output` must have the _exact_ length needed, and the decoded `slice` +/// is not returned. +/// +/// # Errors +/// +/// Errors iff [`decode_buffer`] errors. Note that since a `panic` occurs when `output.len()` is not the +/// exact length needed, [`DecodeErr::BufferLen`] is not possible in addition to [`DecodeErr::TryReserve`]. +/// +/// # Panics +/// +/// `panic`s iff `output` does not have the _exact_ length needed to contain the decoded data. Note when `input` +/// contains an invalid length, [`DecodeErr::EncodedLen`] is returned _not_ a `panic`. +/// +/// # Examples +/// +/// ``` +/// # use base64url_nopad::DecodeErr; +/// assert_eq!( +/// base64url_nopad::decode_buffer_exact([0; 1].as_slice(), [0; 0].as_mut_slice()).unwrap_err(), +/// DecodeErr::EncodedLen +/// ); +/// assert_eq!( +/// base64url_nopad::decode_buffer_exact([0; 2].as_slice(), [0; 1].as_mut_slice()).unwrap_err(), +/// DecodeErr::InvalidByte +/// ); +/// assert_eq!( +/// base64url_nopad::decode_buffer_exact(b"-8", [0; 1].as_mut_slice()).unwrap_err(), +/// DecodeErr::TrailingBits +/// ); +/// let mut buffer = [0; base64url_nopad::decode_len(b"C8Aa_A--91VZbx0".len()).unwrap()]; +/// base64url_nopad::decode_buffer(b"C8Aa_A--91VZbx0", &mut buffer)?; +/// assert_eq!(buffer, [0x0b, 0xc0, 0x1a, 0xfc, 0x0f, 0xbe, 0xf7, b'U', b'Y', b'o', 0x1d]); +/// // Uncommenting below will cause a `panic` since a larger output buffer than necessary is _not_ OK. +/// // base64url_nopad::decode_buffer_exact(b"C8Aa_A--91VZbx0", &mut [0; 128])?; +/// # Ok::<_, DecodeErr>(()) +/// ``` +#[expect( + clippy::panic_in_result_fn, + reason = "purpose of this function is to panic when output does not have the exact length needed" +)] +#[inline] +pub const fn decode_buffer_exact(input: &[u8], output: &mut [u8]) -> Result<(), DecodeErr> { + if let Some(output_len) = decode_len(input.len()) { + assert!( + output.len() == output_len, + "decode_buffer_exact must be passed an output buffer whose length is exactly the length needed to decode the data" + ); + if let Err(e) = decode_buffer(input, output) { + Err(e) + } else { + Ok(()) + } + } else { + Err(DecodeErr::EncodedLen) + } +} +#[cfg(test)] +mod test { + use super::MAX_ENCODE_INPUT_LEN; + #[cfg(feature = "alloc")] + use alloc::string::String; + #[cfg(feature = "alloc")] + use core::fmt; + use rand::{Rng as _, SeedableRng as _, rngs::SmallRng}; + #[cfg(any( + target_pointer_width = "16", + target_pointer_width = "32", + target_pointer_width = "64", + ))] + #[ignore] + #[test] + fn encode_decode_len() { + assert_eq!(MAX_ENCODE_INPUT_LEN, 3 * (usize::MAX.div_ceil(4)) - 1); + let mut rng = SmallRng::from_os_rng(); + for _ in 0..10_000_000 { + #[cfg(target_pointer_width = "16")] + let len = rng.random::<u16>() as usize; + #[cfg(target_pointer_width = "32")] + let len = rng.random::<u32>() as usize; + #[cfg(target_pointer_width = "64")] + let len = rng.random::<u64>() as usize; + if len <= MAX_ENCODE_INPUT_LEN { + assert!( + super::encode_len_checked(len) + .is_some_and(|l| super::decode_len(l).is_some_and(|orig| orig == len)) + ); + } else { + assert!(super::encode_len_checked(len).is_none()); + } + } + for i in 0..1025 { + assert!( + super::encode_len_checked(i) + .is_some_and(|l| super::decode_len(l).is_some_and(|orig| orig == i)) + ); + } + #[cfg(target_pointer_width = "16")] + for i in MAX_ENCODE_INPUT_LEN + 1.. { + assert!(super::encode_len_checked(i).is_none()); + } + #[cfg(not(target_pointer_width = "16"))] + for i in MAX_ENCODE_INPUT_LEN + 1..MAX_ENCODE_INPUT_LEN + 1_000_000 { + assert!(super::encode_len_checked(i).is_none()); + } + assert!(super::encode_len_checked(usize::MAX).is_none()); + assert!(super::encode_len_checked(MAX_ENCODE_INPUT_LEN).is_some_and(|l| l == usize::MAX)); + for _ in 0..10_000_000 { + #[cfg(target_pointer_width = "16")] + let len = rng.random::<u16>() as usize; + #[cfg(target_pointer_width = "32")] + let len = rng.random::<u32>() as usize; + #[cfg(target_pointer_width = "64")] + let len = rng.random::<u64>() as usize; + if len % 4 == 1 { + assert!(super::decode_len(len).is_none()); + } else { + assert!( + super::decode_len(len).is_some_and( + |l| super::encode_len_checked(l).is_some_and(|orig| orig == len) + ) + ); + } + } + for i in 0..1025 { + if i % 4 == 1 { + assert!(super::decode_len(i).is_none()); + } else { + assert!( + super::decode_len(i).is_some_and( + |l| super::encode_len_checked(l).is_some_and(|orig| orig == i) + ) + ); + } + } + #[cfg(target_pointer_width = "16")] + for i in 0..=usize::MAX { + if i % 4 == 1 { + assert!(super::decode_len(i).is_none()); + } else { + assert!(super::decode_len(i).is_some_and(|l| super::encode_len_checked(l).is_some_and(|orig| orig == i))); + } + } + #[cfg(not(target_pointer_width = "16"))] + for i in usize::MAX - 1_000_000..=usize::MAX { + if i % 4 == 1 { + assert!(super::decode_len(i).is_none()); + } else { + assert!(super::decode_len(i).is_some_and(|l| super::encode_len_checked(l).is_some_and(|orig| orig == i))); + } + } + assert!(super::decode_len(usize::MAX).is_some_and(|l| l == MAX_ENCODE_INPUT_LEN)); + } + #[cfg(feature = "alloc")] + #[test] + fn encode_write() -> fmt::Result { + let input = [9; 8192]; + let mut buffer = String::with_capacity(super::encode_len(input.len())); + let cap = buffer.capacity() as isize; + let mut write_len; + for len in 0..input.len() { + write_len = super::encode_len(len) as isize; + match write_len.checked_add(buffer.len() as isize) { + None => { + buffer.clear(); + super::encode_write(&input[..len], &mut buffer)?; + assert_eq!(buffer.len() as isize, write_len); + } + Some(l) => { + if l > cap { + buffer.clear(); + super::encode_write(&input[..len], &mut buffer)?; + assert_eq!(buffer.len() as isize, write_len); + } else { + super::encode_write(&input[..len], &mut buffer)?; + assert_eq!(buffer.len() as isize, l); + } + } + } + assert!( + buffer + .as_bytes() + .iter() + .all(|b| { matches!(*b, b'C' | b'J' | b'Q' | b'k') }) + ); + } + Ok(()) + } +}