commit ab3ee50ee16c736370e8511497c661fdc915b190
Author: Zack Newman <zack@philomathiclife.com>
Date: Fri, 8 Aug 2025 14:05:35 -0600
init
Diffstat:
| A | .gitignore | | | 2 | ++ |
| A | Cargo.toml | | | 102 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | LICENSE-APACHE | | | 177 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | LICENSE-MIT | | | 20 | ++++++++++++++++++++ |
| A | README.md | | | 87 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/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) [![crates-io]](https://crates.io/crates/base64url_nopad) [![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(())
+ }
+}