commit 5faebf1a905c3acb2ad545e183b631d91e6d3d2c
Author: Zack Newman <zack@philomathiclife.com>
Date: Sun, 12 Jul 2026 13:54:19 -0600
init
Diffstat:
| A | .gitignore | | | 2 | ++ |
| A | Cargo.toml | | | 123 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | LICENSE-APACHE | | | 177 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | LICENSE-MIT | | | 20 | ++++++++++++++++++++ |
| A | README.md | | | 89 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/lib.rs | | | 1860 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/tests.rs | | | 275 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
7 files changed, 2546 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,123 @@
+[package]
+authors = ["Zack Newman <zack@philomathiclife.com>"]
+categories = ["encoding", "no-std::no-alloc"]
+description = "Fast, efficient, correct, and const base64 with padding encoding and decoding"
+documentation = "https://docs.rs/base64_pad/latest/base64_pad/"
+edition = "2024"
+keywords = ["base64", "no-std"]
+license = "MIT OR Apache-2.0"
+name = "base64_pad"
+readme = "README.md"
+repository = "https://git.philomathiclife.com/repos/base64_pad/"
+rust-version = "1.97.0"
+version = "0.1.0"
+
+[lints.rust]
+deprecated-safe = { level = "deny", priority = -1 }
+future-incompatible = { level = "deny", priority = -1 }
+keyword-idents = { level = "deny", priority = -1 }
+let-underscore = { level = "deny", priority = -1 }
+nonstandard-style = { 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 }
+unknown-or-malformed-diagnostic-attributes = { level = "deny", priority = -1 }
+unused = { level = "deny", priority = -1 }
+warnings = { level = "deny", priority = -1 }
+ambiguous-negative-literals = { level = "deny", priority = -1 }
+closure-returning-async-block = { level = "deny", priority = -1 }
+dead-code-pub-in-binary = { level = "deny", priority = -1 }
+deprecated-in-future = { level = "deny", priority = -1 }
+#deprecated-llvm-intrinsic = { level = "deny", priority = -1 }
+deref-into-dyn-supertrait = { level = "deny", priority = -1 }
+ffi-unwind-calls = { level = "deny", priority = -1 }
+#fuzzy-provenance-casts = { level = "deny", priority = -1 }
+impl-trait-redundant-captures = { level = "deny", priority = -1 }
+linker-info = { 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 }
+redundant-imports = { level = "deny", priority = -1 }
+redundant-lifetimes = { level = "deny", priority = -1 }
+#resolving-to-items-shadowing-supertrait-items = { level = "deny", priority = -1 }
+#shadowing-supertrait-items = { level = "deny", priority = -1 }
+single-use-lifetimes = { 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-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 }
+
+[lints.clippy]
+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"
+inline_trait_bounds = "allow"
+min_ident_chars = "allow"
+missing_trait_methods = "allow"
+question_mark_used = "allow"
+ref_patterns = "allow"
+return_and_then = "allow"
+single_call_fn = "allow"
+single_char_lifetime_names = "allow"
+unseparated_literal_suffix = "allow"
+
+[lints.rustdoc]
+all = { level = "deny", priority = -1 }
+
+[package.metadata.docs.rs]
+all-features = true
+default-target = "x86_64-unknown-linux-gnu"
+targets = [
+ "aarch64-apple-darwin",
+ "aarch64-pc-windows-msvc",
+ "aarch64-unknown-linux-gnu",
+ "i686-pc-windows-msvc",
+ "i686-unknown-linux-gnu",
+ "x86_64-pc-windows-gnu",
+ "x86_64-pc-windows-msvc",
+ "x86_64-unknown-freebsd",
+ "x86_64-unknown-linux-musl",
+ "x86_64-unknown-netbsd"
+]
+
+[dev-dependencies]
+rand = { version = "0.10.2", default-features = false, features = ["sys_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 © 2026 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,89 @@
+# `base64_pad`
+
+[<img alt="git" src="https://git.philomathiclife.com/badges/base64_pad.svg" height="20">](https://git.philomathiclife.com/base64_pad/log.html)
+[<img alt="crates.io" src="https://img.shields.io/crates/v/base64_pad.svg?style=for-the-badge&color=fc8d62&logo=rust" height="20">](https://crates.io/crates/base64_pad)
+[<img alt="docs.rs" src="https://img.shields.io/badge/docs.rs-base64_pad-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs" height="20">](https://docs.rs/base64_pad/latest/base64_pad/)
+
+`base64_pad` is a library for fast, efficient, correct, and `const` encoding and decoding of base64 with
+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.
+
+## `base64_pad` in action
+
+```rust
+use base64_pad::DecodeErr;
+/// Length of our input to encode.
+const INPUT_LEN: usize = 259;
+/// The base64-encoded value with 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; base64_pad::encode_len(INPUT_LEN)];
+ assert_eq!(base64_pad::encode_buffer(&input, &mut output), ENCODED_VAL);
+ assert_eq!(base64_pad::decode_len(output.len()), Some(INPUT_LEN + 2));
+ base64_pad::decode_buffer(ENCODED_VAL.as_bytes(), &mut output[..INPUT_LEN])?;
+ assert_eq!(input, output[..INPUT_LEN]);
+ Ok(())
+}
+```
+
+## 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 --all-targets`, `cargo test --all-targets -- --include-ignored`, and
+`cargo test --doc` should be run _for each possible combination of "features"_ using the stable and MSRV toolchains.
+One easy way to achieve this is by invoking [`ci-cargo`](https://crates.io/crates/ci-cargo) as
+`ci-cargo clippy --all-targets test --all-targets --include-ignored` in the `base64_pad` directory.
+
+Last, `RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features` should be run to ensure documentation can be
+built.
+
+### Status
+
+The crate is only tested on the `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,1860 @@
+//! [![git]](https://git.philomathiclife.com/base64_pad/log.html) [![crates-io]](https://crates.io/crates/base64_pad) [![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
+//!
+//! `base64_pad` is a library for fast, efficient, correct, and `const` encoding and decoding of base64
+//! with 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.
+//!
+//! ## `base64_pad` in action
+//!
+//! ```
+//! # use base64_pad::DecodeErr;
+//! /// Length of our input to encode.
+//! const INPUT_LEN: usize = 259;
+//! /// The base64-encoded value with 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; base64_pad::encode_len(INPUT_LEN)];
+//! assert_eq!(base64_pad::encode_buffer(&input, &mut output), ENCODED_VAL);
+//! assert_eq!(base64_pad::decode_len(output.len()), Some(INPUT_LEN + 2));
+//! base64_pad::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).
+#![expect(
+ clippy::doc_paragraphs_missing_punctuation,
+ reason = "false positive for crate documentation having image links"
+)]
+#![cfg_attr(
+ all(
+ test,
+ target_os = "macos",
+ any(
+ target_pointer_width = "16",
+ target_pointer_width = "32",
+ target_pointer_width = "64"
+ )
+ ),
+ allow(linker_info, reason = "getrandom causes linker-info to fire on macos")
+)]
+#![no_std]
+#![cfg_attr(docsrs, feature(doc_cfg))]
+#[cfg(any(doc, feature = "alloc"))]
+extern crate alloc;
+/// Unit tests.
+#[cfg(test)]
+mod tests;
+#[cfg(any(doc, feature = "alloc"))]
+use alloc::{collections::TryReserveError, string::String, vec::Vec};
+use core::{
+ error::Error,
+ fmt::{self, Display, Formatter, Write},
+ hint::cold_path,
+ mem,
+};
+/// The base64 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,
+ /// +.
+ Plus,
+ /// /.
+ Slash,
+}
+/// Sorted ASCII `u8`s for [`Alphabet`].
+const ASCII: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+/// Sorted `char`s for [`Alphabet`].
+const CHARS: &[char; 64] = &[
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
+ 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
+ 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4',
+ '5', '6', '7', '8', '9', '+', '/',
+];
+/// [`Alphabet`] variants indexed based on the their ASCII representation.
+const FROM_ASCII: &[Option<Alphabet>; 256] = &[
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ Some(Alphabet::Plus),
+ None,
+ None,
+ None,
+ Some(Alphabet::Slash),
+ Some(Alphabet::Zero),
+ Some(Alphabet::One),
+ Some(Alphabet::Two),
+ Some(Alphabet::Three),
+ Some(Alphabet::Four),
+ Some(Alphabet::Five),
+ Some(Alphabet::Six),
+ Some(Alphabet::Seven),
+ Some(Alphabet::Eight),
+ Some(Alphabet::Nine),
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ Some(Alphabet::A),
+ Some(Alphabet::B),
+ Some(Alphabet::C),
+ Some(Alphabet::D),
+ Some(Alphabet::E),
+ Some(Alphabet::F),
+ Some(Alphabet::G),
+ Some(Alphabet::H),
+ Some(Alphabet::I),
+ Some(Alphabet::J),
+ Some(Alphabet::K),
+ Some(Alphabet::L),
+ Some(Alphabet::M),
+ Some(Alphabet::N),
+ Some(Alphabet::O),
+ Some(Alphabet::P),
+ Some(Alphabet::Q),
+ Some(Alphabet::R),
+ Some(Alphabet::S),
+ Some(Alphabet::T),
+ Some(Alphabet::U),
+ Some(Alphabet::V),
+ Some(Alphabet::W),
+ Some(Alphabet::X),
+ Some(Alphabet::Y),
+ Some(Alphabet::Z),
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ Some(Alphabet::a),
+ Some(Alphabet::b),
+ Some(Alphabet::c),
+ Some(Alphabet::d),
+ Some(Alphabet::e),
+ Some(Alphabet::f),
+ Some(Alphabet::g),
+ Some(Alphabet::h),
+ Some(Alphabet::i),
+ Some(Alphabet::j),
+ Some(Alphabet::k),
+ Some(Alphabet::l),
+ Some(Alphabet::m),
+ Some(Alphabet::n),
+ Some(Alphabet::o),
+ Some(Alphabet::p),
+ Some(Alphabet::q),
+ Some(Alphabet::r),
+ Some(Alphabet::s),
+ Some(Alphabet::t),
+ Some(Alphabet::u),
+ Some(Alphabet::v),
+ Some(Alphabet::w),
+ Some(Alphabet::x),
+ Some(Alphabet::y),
+ Some(Alphabet::z),
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+];
+impl Alphabet {
+ /// Returns `Self` that corresponds to `b`.
+ ///
+ /// `Some` is returned iff `b` is in `0..=63`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use base64_pad::Alphabet;
+ /// assert_eq!(Alphabet::from_u8(25), Some(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::Slash`.
+ if b <= Self::Slash as u8 {
+ // SAFETY:
+ // Our safety precondition is that `b` is in-range.
+ Some(unsafe { mem::transmute::<u8, Self>(b) })
+ } else {
+ None
+ }
+ }
+ /// Returns the `u8` `self` represents.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use base64_pad::Alphabet;
+ /// assert_eq!(Alphabet::Plus.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 base64_pad::Alphabet;
+ /// assert_eq!(Alphabet::c.to_ascii(), b'c');
+ /// ```
+ #[expect(
+ clippy::as_conversions,
+ clippy::indexing_slicing,
+ reason = "comments justify correctness"
+ )]
+ #[inline]
+ #[must_use]
+ pub const fn to_ascii(self) -> u8 {
+ // `u8 as usize` is always OK; and we want this to be `const` so can't rely on `usize::from`.
+ // `self.to_u8() < 64` and `ASCII.len() == 64`, so indexing can't `panic`.
+ ASCII[self.to_u8() as usize]
+ }
+ /// Returns `Some` iff `ascii` is the ASCII representation of `Self`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use base64_pad::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());
+ /// }
+ /// }
+ /// ```
+ #[expect(
+ clippy::as_conversions,
+ clippy::indexing_slicing,
+ reason = "comments justify correctness"
+ )]
+ #[inline]
+ #[must_use]
+ pub const fn from_ascii(ascii: u8) -> Option<Self> {
+ // `u8 as usize` is always OK; and we want this to be `const` so can't rely on `usize::from`.
+ // `FROM_ASCII` has length 256, so indexing can't `panic`.
+ FROM_ASCII[ascii as usize]
+ }
+ /// Same as [`Self::to_ascii`] except a `char` is returned.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use base64_pad::Alphabet;
+ /// assert_eq!(Alphabet::J.to_char(), 'J');
+ /// ```
+ #[expect(
+ clippy::as_conversions,
+ clippy::indexing_slicing,
+ reason = "comments justify correctness"
+ )]
+ #[inline]
+ #[must_use]
+ pub const fn to_char(self) -> char {
+ // `u8 as usize` is always OK; and we want this to be `const` so can't rely on `usize::from`.
+ // `self.to_u8() < 64` and `CHARS.len() == 64`, so indexing can't `panic`.
+ CHARS[self.to_u8() as usize]
+ }
+ /// Same as [`Self::from_ascii`] except the input is a `char`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use base64_pad::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());
+ /// }
+ /// }
+ /// ```
+ #[expect(
+ clippy::as_conversions,
+ clippy::cast_possible_truncation,
+ reason = "comments justify correctness"
+ )]
+ #[inline]
+ #[must_use]
+ pub const fn from_char(c: char) -> Option<Self> {
+ // `char as u32` is always OK.
+ let code_point = c as u32;
+ if code_point < 256 {
+ // We just verified `code_point` does not exceed `u8::MAX`, so `code_point as u8` is lossless.
+ Self::from_ascii(code_point as u8)
+ } else {
+ 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()
+ }
+}
+/// Returns the exact number of bytes needed to encode an input of length `input_length` _without_ padding.
+///
+/// Calling code must ensure `input_length <= MAX_ENCODE_INPUT_LEN`.
+#[expect(
+ clippy::arithmetic_side_effects,
+ clippy::integer_division,
+ clippy::integer_division_remainder_used,
+ reason = "proof and comment justifies their correctness"
+)]
+const fn encode_len_internal(input_length: usize) -> usize {
+ // 256^n is the number of distinct values of the input. Let the base64 encoding with 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:
+ // `MAX_ENCODE_INPUT_LEN` = decode_len(usize::MAX).unwrap(); thus all values less than or equal to
+ // `MAX_ENCODE_INPUT_LEN` won't overflow ignoring intermediate calcuations since ⌈4n/3⌉ is a
+ // monotonically increasing function.
+ // 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.
+ // (n / 3) << 2 <= m <= usize::MAX; thus the left operand of + is fine.
+ // n % 3 <= 2
+ // <==>
+ // 4(n % 3) <= 8 < usize::MAX; thus (n % 3) << 2 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) << 2) + ((n % 3) << 2).div_ceil(3)
+ // m.next_multiple_of(4) <= usize::MAX since calling code must ensure
+ // `input_length <= MAX_ENCODE_INPUT_LEN`.
+ ((input_length / 3) << 2) + ((input_length % 3) << 2).div_ceil(3)
+}
+/// The maximum value [`encode_len_checked`] will accept before returning `None`.
+// This won't `panic` since `usize::MAX - 3` ≡ 0 (mod 4).
+pub const MAX_ENCODE_INPUT_LEN: usize = decode_len(usize::MAX - 3).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 base64_pad::MAX_ENCODE_INPUT_LEN;
+/// assert!(base64_pad::encode_len_checked(usize::MAX).is_none());
+/// assert!(base64_pad::encode_len_checked(MAX_ENCODE_INPUT_LEN + 1).is_none());
+/// assert_eq!(base64_pad::encode_len_checked(MAX_ENCODE_INPUT_LEN), Some(usize::MAX - 3));
+/// assert_eq!(base64_pad::encode_len_checked(3), Some(4));
+/// assert_eq!(base64_pad::encode_len_checked(2), Some(4));
+/// assert_eq!(base64_pad::encode_len_checked(1), Some(4));
+/// assert_eq!(base64_pad::encode_len_checked(0), Some(0));
+/// ```
+#[inline]
+#[must_use]
+pub const fn encode_len_checked(input_length: usize) -> Option<usize> {
+ if input_length <= MAX_ENCODE_INPUT_LEN {
+ // This won't overflow since `input_length <= MAX_ENCODE_INPUT_LEN`.
+ Some(encode_len_internal(input_length).next_multiple_of(4))
+ } else {
+ cold_path();
+ 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 base64_pad::MAX_ENCODE_INPUT_LEN;
+/// // Uncommenting below will cause a `panic`.
+/// // base64_pad::encode_len(usize::MAX - 4);
+/// // Uncommenting below will cause a `panic`.
+/// // base64_pad::encode_len(MAX_ENCODE_INPUT_LEN + 1);
+/// assert_eq!(base64_pad::encode_len(MAX_ENCODE_INPUT_LEN), usize::MAX - 3);
+/// assert!(base64_pad::encode_len(isize::MAX as usize) > isize::MAX as usize);
+/// assert_eq!(base64_pad::encode_len(3), 4);
+/// assert_eq!(base64_pad::encode_len(2), 4);
+/// assert_eq!(base64_pad::encode_len(1), 4);
+/// assert_eq!(base64_pad::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()
+}
+/// `const`-version of `unreachable`.
+macro_rules! impossible {
+ ( $( $x:literal)? ) => {
+ {
+ cold_path();
+ $(
+ panic!($x);
+ )?
+ }
+ };
+}
+/// Encodes `input` _without padding_ into `output` re-interpreting the encoded subset of `output` as a `str`
+/// before returning it.
+///
+/// # Panics
+///
+/// `panic`s iff `output` is not large enough to contained the encoded data.
+#[expect(unsafe_code, reason = "comments justify correctness")]
+#[expect(
+ clippy::missing_asserts_for_indexing,
+ reason = "trust the compiler to already optimize since we match on the length"
+)]
+#[expect(
+ clippy::arithmetic_side_effects,
+ clippy::as_conversions,
+ clippy::indexing_slicing,
+ reason = "comments justify correctness"
+)]
+const fn encode_buffer_internal<'a>(input: &[u8], output: &'a mut [u8]) -> &'a mut str {
+ let (mut chunks, rem) = input.as_chunks::<3>();
+ let (mut fst, mut snd, mut third);
+ let mut output_idx = 0;
+ // There is a _substantial_ boost in performance if we chunk encode.
+ while let [first, ref rest @ ..] = *chunks {
+ (fst, snd, third) = (first[0], first[1], first[2]);
+ // We trim the last two bits and interpret `fst` as a 6-bit integer.
+ // `u8 as usize` is always OK; and we want this to be `const` so `usize::from` won't work.
+ // `ASCII.len() == 64 > fst >> 2u8`, so indexing won't `panic`.
+ output[output_idx] = ASCII[(fst >> 2u8) as usize];
+ // The two bits we trimmed are the first two bits of the next 6-bit integer.
+ output_idx += 1;
+ // We trim the last four bits and interpret `snd` as a 6-bit integer.
+ // The first two bits are the trailing 2 bits from the previous value.
+ // `u8 as usize` is always OK; and we want this to be `const` so `usize::from` won't work.
+ // `ASCII.len() == 64 > ((fst & 3) << 4u8) | (snd >> 4u8)`, so indexing won't `panic`.
+ output[output_idx] = ASCII[(((fst & 3) << 4u8) | (snd >> 4u8)) as usize];
+ output_idx += 1;
+ // We trim the last six bits and interpret `third` as a 6-bit integer.
+ // The first four bits are the trailing 4 bits from the previous value.
+ // `u8 as usize` is always OK; and we want this to be `const` so `usize::from` won't work.
+ // `ASCII.len() == 64 > ((snd & 15) << 2u8) | (third >> 6u8)`, so indexing won't `panic`.
+ output[output_idx] = ASCII[(((snd & 15) << 2u8) | (third >> 6u8)) as usize];
+ // Every third `u8` corresponds to a fourth base64 `u8`.
+ output_idx += 1;
+ // `u8 as usize` is always OK; and we want this to be `const` so `usize::from` won't work.
+ // `ASCII.len() == 64 > (third & 63)`, so indexing won't `panic`.
+ output[output_idx] = ASCII[(third & 63) as usize];
+ output_idx += 1;
+ chunks = rest;
+ }
+ match rem.len() {
+ 0 => {}
+ 1 => {
+ // `rem.len() == 1`, so indexing won't `panic`.
+ fst = rem[0];
+ // `u8 as usize` is always OK; and we want this to be `const` so `usize::from` won't work.
+ // `ASCII.len() == 64 > fst >> 2u8`, so indexing won't `panic`.
+ output[output_idx] = ASCII[(fst >> 2u8) as usize];
+ output_idx += 1;
+ // `ASCII.len() == 64 > (fst & 3) << 4u8`, so indexing won't `panic`.
+ output[output_idx] = ASCII[((fst & 3) << 4u8) as usize];
+ output_idx += 1;
+ }
+ 2 => {
+ // `rem.len() == 2`, so indexing won't `panic`.
+ (fst, snd) = (rem[0], rem[1]);
+ // `input.len()` is not a multiple of 3; thus we have to append a final `u8` containing the
+ // last bits.
+ // `u8 as usize` is always OK; and we want this to be `const` so `usize::from` won't work.
+ // `ASCII.len() == 64 > fst >> 2u8`, so indexing won't `panic`.
+ output[output_idx] = ASCII[(fst >> 2u8) as usize];
+ output_idx += 1;
+ // `ASCII.len() == 64 > ((fst & 3) << 4u8) | (snd >> 4u8)`, so indexing won't `panic`.
+ output[output_idx] = ASCII[(((fst & 3) << 4u8) | (snd >> 4u8)) as usize];
+ output_idx += 1;
+ // `ASCII.len() == 64 > (snd & 15) << 2u8`, so indexing won't `panic`.
+ output[output_idx] = ASCII[((snd & 15) << 2u8) as usize];
+ output_idx += 1;
+ }
+ _ => impossible!("there is a bug in core::slice::as_chunks"),
+ }
+ // SAFETY:
+ // `output_idx <= output.len()`.
+ let val = unsafe { output.split_at_mut_unchecked(output_idx) }.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.
+ unsafe { str::from_utf8_unchecked_mut(val) }
+}
+/// 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_eq!(
+/// base64_pad::encode_buffer_checked([0; 0].as_slice(), [0; 0].as_mut_slice()).as_deref(), Some("")
+/// );
+/// assert_eq!(
+/// base64_pad::encode_buffer_checked([0; 1].as_slice(), [0; 4].as_mut_slice()).as_deref(), Some("AA==")
+/// );
+/// // A larger output buffer than necessary is OK.
+/// assert_eq!(
+/// base64_pad::encode_buffer_checked([1; 1].as_slice(), [0; 128].as_mut_slice()).as_deref(), Some("AQ==")
+/// );
+/// assert_eq!(
+/// base64_pad::encode_buffer_checked(
+/// [0xc9; 14].as_slice(),
+/// [0; base64_pad::encode_len(14)].as_mut_slice()
+/// ).as_deref(),
+/// Some("ycnJycnJycnJycnJyck=")
+/// );
+/// assert!(base64_pad::encode_buffer_checked([0; 1].as_slice(), [0; 3].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>(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 {
+ match encode_buffer_internal(input, output).len() & 3 {
+ 0 => {}
+ 1 => impossible!("there is a bug in base64_pad::encode_buffer_internal"),
+ 2 => {
+ // The encoded data must be padded with two `b'='`; so underflow can't happen nor will
+ // indexing `panic`.
+ output[final_len - 1] = b'=';
+ output[final_len - 2] = b'=';
+ }
+ 3 => {
+ // The encoded data must be padded with one `b'='`; so underflow can't happen nor will
+ // indexing `panic`.
+ output[final_len - 1] = b'=';
+ }
+ _ => impossible!("usize & 3 is greater than 3"),
+ }
+ // 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 {
+ cold_path();
+ 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!(
+/// base64_pad::encode_buffer([0; 0].as_slice(), [0; 0].as_mut_slice()),
+/// ""
+/// );
+/// assert_eq!(
+/// base64_pad::encode_buffer([0; 1].as_slice(), [0; 4].as_mut_slice()),
+/// "AA=="
+/// );
+/// // A larger output buffer than necessary is OK.
+/// assert_eq!(
+/// base64_pad::encode_buffer([255; 1].as_slice(), [0; 256].as_mut_slice()),
+/// "/w=="
+/// );
+/// assert_eq!(
+/// base64_pad::encode_buffer(
+/// [0xc9; 14].as_slice(),
+/// [0; base64_pad::encode_len(14)].as_mut_slice()
+/// ),
+/// "ycnJycnJycnJycnJyck="
+/// );
+/// // The below will `panic` when uncommented since the supplied output buffer is too small.
+/// // _ = base64_pad::encode_buffer([0; 1].as_slice(), [0; 3].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!(
+/// base64_pad::try_encode([0; 0].as_slice())?,
+/// ""
+/// );
+/// assert_eq!(
+/// base64_pad::try_encode([0; 1].as_slice())?,
+/// "AA=="
+/// );
+/// assert_eq!(
+/// base64_pad::try_encode([128, 40, 3].as_slice())?,
+/// "gCgD"
+/// );
+/// assert_eq!(
+/// base64_pad::try_encode([0x7b; 22].as_slice())?,
+/// "e3t7e3t7e3t7e3t7e3t7e3t7e3t7ew=="
+/// );
+/// # Ok::<_, TryReserveError>(())
+/// ```
+#[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!(
+/// base64_pad::encode([0; 0].as_slice()),
+/// ""
+/// );
+/// assert_eq!(
+/// base64_pad::encode([0; 1].as_slice()),
+/// "AA=="
+/// );
+/// assert_eq!(
+/// base64_pad::encode([128, 40, 3].as_slice()),
+/// "gCgD"
+/// );
+/// assert_eq!(
+/// base64_pad::encode([0x7b; 22].as_slice()),
+/// "e3t7e3t7e3t7e3t7e3t7e3t7e3t7ew=="
+/// );
+/// ```
+#[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 base64 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();
+/// base64_pad::encode_write([0; 0].as_slice(), &mut buffer)?;
+/// assert_eq!(buffer, "");
+/// buffer.clear();
+/// base64_pad::encode_write([0; 1].as_slice(), &mut buffer)?;
+/// assert_eq!(buffer, "AA==");
+/// buffer.clear();
+/// base64_pad::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], mut writer: W) -> fmt::Result {
+ /// 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 = 1024;
+ /// 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` ≡ 0 (mod 4)).
+ const INPUT_LEN: usize = decode_len(MAX_BUFFER_LEN).unwrap();
+ let mut buffer = [0; MAX_BUFFER_LEN];
+ // This won't `panic` since `input.len()` is guaranteed to be no more than `isize::MAX` which is less than
+ // `MAX_ENCODE_INPUT_LEN`.
+ let no_pad_len = encode_len_internal(input.len());
+ // This won't overflow since `input.len()` is guaranteed to be no more than `isize::MAX` which is less than
+ // `MAX_ENCODE_INPUT_LEN`.
+ let len = no_pad_len.next_multiple_of(4);
+ if len <= 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]))
+ } else {
+ let mut counter = 0;
+ // `no_pad_len / MAX_BUFFER_LEN` is equal to ⌊no_pad_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 = no_pad_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_internal` 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_internal(
+ 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 base64 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!(
+/// base64_pad::try_encode_append([0; 0].as_slice(), &mut buffer)?,
+/// ""
+/// );
+/// assert_eq!(
+/// base64_pad::try_encode_append([0; 1].as_slice(), &mut buffer)?,
+/// "AA=="
+/// );
+/// assert_eq!(
+/// base64_pad::try_encode_append([128, 40, 3].as_slice(), &mut buffer)?,
+/// "gCgD"
+/// );
+/// assert_eq!(buffer, "AA==gCgD");
+/// assert_eq!(
+/// base64_pad::try_encode_append([0x7b; 22].as_slice(), &mut buffer)?,
+/// "e3t7e3t7e3t7e3t7e3t7e3t7e3t7ew=="
+/// );
+/// assert_eq!(buffer, "AA==gCgDe3t7e3t7e3t7e3t7e3t7e3t7e3t7ew==");
+/// # Ok::<_, TryReserveError>(())
+/// ```
+#[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 base64 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();
+/// base64_pad::try_encode_append_only([0; 0].as_slice(), &mut buffer)?;
+/// assert_eq!(buffer, "");
+/// base64_pad::try_encode_append_only([0; 1].as_slice(), &mut buffer)?;
+/// assert_eq!(buffer, "AA==");
+/// base64_pad::try_encode_append_only([128, 40, 3].as_slice(), &mut buffer)?;
+/// assert_eq!(buffer, "AA==gCgD");
+/// base64_pad::try_encode_append_only([0x7b; 22].as_slice(), &mut buffer)?;
+/// assert_eq!(buffer, "AA==gCgDe3t7e3t7e3t7e3t7e3t7e3t7e3t7ew==");
+/// # Ok::<_, TryReserveError>(())
+/// ```
+#[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!(
+/// base64_pad::encode_append([0; 0].as_slice(), &mut buffer),
+/// ""
+/// );
+/// assert_eq!(
+/// base64_pad::encode_append([0; 1].as_slice(), &mut buffer),
+/// "AA=="
+/// );
+/// assert_eq!(
+/// base64_pad::encode_append([128, 40, 3].as_slice(), &mut buffer),
+/// "gCgD"
+/// );
+/// assert_eq!(buffer, "AA==gCgD");
+/// assert_eq!(
+/// base64_pad::encode_append([0x7b; 22].as_slice(), &mut buffer),
+/// "e3t7e3t7e3t7e3t7e3t7e3t7e3t7ew=="
+/// );
+/// assert_eq!(buffer, "AA==gCgDe3t7e3t7e3t7e3t7e3t7e3t7e3t7ew==");
+/// ```
+#[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();
+/// base64_pad::encode_append_only([0; 0].as_slice(), &mut buffer);
+/// assert_eq!(buffer, "");
+/// base64_pad::encode_append_only([0; 1].as_slice(), &mut buffer);
+/// assert_eq!(buffer, "AA==");
+/// base64_pad::encode_append_only([128, 40, 3].as_slice(), &mut buffer);
+/// assert_eq!(buffer, "AA==gCgD");
+/// base64_pad::encode_append_only([0x7b; 22].as_slice(), &mut buffer);
+/// assert_eq!(buffer, "AA==gCgDe3t7e3t7e3t7e3t7e3t7e3t7e3t7ew==");
+/// # Ok::<_, TryReserveError>(())
+/// ```
+#[cfg(feature = "alloc")]
+#[inline]
+pub fn encode_append_only(input: &[u8], s: &mut String) {
+ _ = encode_append(input, s);
+}
+/// Returns the maximum number of bytes an encoded value of length `input_length` corresponds to when decoded.
+///
+/// `Some` is returned iff `input_length` represents a possible length of a base64 with padding input.
+///
+/// Note due to padding, this may return a value that is too large. When used to calculate the length needed
+/// for `output` in [`decode_buffer_exact`], one must subtract the number of trailing `b'='` from the returned
+/// value keeping in mind that an encoded value can have at most two trailing `b'='`.
+///
+/// # Examples
+///
+/// ```
+/// # use base64_pad::MAX_ENCODE_INPUT_LEN;
+/// assert!(base64_pad::decode_len(1).is_none());
+/// assert!(base64_pad::decode_len(2).is_none());
+/// assert!(base64_pad::decode_len(3).is_none());
+/// assert!(base64_pad::decode_len(usize::MAX).is_none());
+/// assert!(base64_pad::decode_len(usize::MAX - 1).is_none());
+/// assert!(base64_pad::decode_len(usize::MAX - 2).is_none());
+/// assert_eq!(base64_pad::decode_len(usize::MAX - 3), Some(MAX_ENCODE_INPUT_LEN));
+/// assert_eq!(base64_pad::decode_len(4), Some(3));
+/// assert_eq!(base64_pad::decode_len(0), Some(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> {
+ // Encoded-values always have a length that is a multiple of 4; thus per `encoded_len_checked`, the decoded
+ // length is 3/4 of it.
+ if input_length.trailing_zeros() > 1 {
+ // We divide by 4 before multiplying by 3; thus this can't overflow.
+ Some(3 * (input_length >> 2))
+ } else {
+ None
+ }
+}
+/// Returns the exact number of bytes needed to decode a base64 input _without_ padding of length `input_length`.
+#[expect(
+ clippy::arithmetic_side_effects,
+ reason = "proof and comment justifies their correctness"
+)]
+const fn decode_no_pad_len(input_length: usize) -> 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`.
+ // 3 * (n >> 2) <= 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 >> 2)) + ((3 * rem) >> 2), and m < usize::MAX.
+ (3 * (input_length >> 2)) + ((3 * (input_length & 3)) >> 2)
+}
+/// Error returned from [`decode_buffer`] and [`decode`].
+///
+/// Note when [`alloc`](./index.html#alloc) is not enabled, [`Copy`] is also implemented.
+#[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(feature = "alloc")]
+ TryReserve(TryReserveError),
+}
+#[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 {}
+/// Trims padding from `input`.
+///
+/// Returns `None` iff `input` does not have a valid length.
+#[expect(unsafe_code, reason = "comment justifies correctness")]
+#[expect(
+ clippy::arithmetic_side_effects,
+ clippy::indexing_slicing,
+ reason = "comments justify correctness"
+)]
+const fn trim_padding(input: &[u8]) -> Option<&[u8]> {
+ let len = input.len();
+ if len.trailing_zeros() > 1 {
+ // `len - 1` won't underflow since `len > 0`, and `input[len - 1]` won't `panic` since that is the
+ // last index of `input`.
+ if len > 0 && input[len - 1] == b'=' {
+ // `len - 2` won't underflow since `len > 0` and is a multiple of 4, and `input[len - 2]` won't `panic`
+ // since `len` is the length of `input`.
+ let l = if input[len - 2] == b'=' {
+ len - 2
+ } else {
+ len - 1
+ };
+ // SAFETY:
+ // `input.len() >= l`.
+ Some(unsafe { input.split_at_unchecked(l) }.0)
+ } else {
+ Some(input)
+ }
+ } else {
+ None
+ }
+}
+/// Decodes `input` into `output` returning the subset of `output` containing the decoded data.
+///
+/// # Errors
+///
+/// Errors iff `output` is not large enough to contain the decoded data or `input` is an invalid base64-encoded
+/// value with padding.
+///
+/// Note [`DecodeErr::TryReserve`] will never be returned.
+///
+/// # Examples
+///
+/// ```
+/// # use base64_pad::DecodeErr;
+/// assert_eq!(base64_pad::decode_buffer(b"", [0; 0].as_mut_slice())?, b"");
+/// assert_eq!(
+/// base64_pad::decode_buffer(b"A", [0; 1].as_mut_slice()).unwrap_err(),
+/// DecodeErr::EncodedLen
+/// );
+/// assert_eq!(
+/// base64_pad::decode_buffer(b"A-==".as_slice(), [0; 1].as_mut_slice()).unwrap_err(),
+/// DecodeErr::InvalidByte
+/// );
+/// assert_eq!(
+/// base64_pad::decode_buffer(b"AA==".as_slice(), [0; 0].as_mut_slice()).unwrap_err(),
+/// DecodeErr::BufferLen
+/// );
+/// assert_eq!(
+/// base64_pad::decode_buffer(b"+8==", [0; 1].as_mut_slice()).unwrap_err(),
+/// DecodeErr::TrailingBits
+/// );
+/// // A larger output buffer than necessary is OK.
+/// assert_eq!(base64_pad::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::missing_asserts_for_indexing,
+ reason = "trust the compiler to already optimize since we match on the length"
+)]
+#[expect(
+ clippy::arithmetic_side_effects,
+ clippy::indexing_slicing,
+ reason = "comments justify correctness"
+)]
+#[expect(clippy::redundant_else, reason = "prefer the elses")]
+#[inline]
+pub const fn decode_buffer<'a>(
+ input: &[u8],
+ output: &'a mut [u8],
+) -> Result<&'a mut [u8], DecodeErr> {
+ if let Some(i) = trim_padding(input) {
+ let output_len = decode_no_pad_len(i.len());
+ if output.len() >= output_len {
+ let mut output_idx = 0;
+ let (mut chunks, rem) = i.as_chunks::<4>();
+ let (mut snd, mut third);
+ // There is a _substantial_ boost in performance if we chunk decode.
+ while let [first, ref rest @ ..] = *chunks {
+ if let Some(base64_fst) = Alphabet::from_ascii(first[0])
+ && let Some(base64_snd) = Alphabet::from_ascii(first[1])
+ && let Some(base64_third) = Alphabet::from_ascii(first[2])
+ && let Some(base64_fourth) = Alphabet::from_ascii(first[3])
+ {
+ (snd, third) = (base64_snd.to_u8(), base64_third.to_u8());
+ output[output_idx] = (base64_fst.to_u8() << 2) | (snd >> 4);
+ output_idx += 1;
+ output[output_idx] = (snd << 4) | (third >> 2);
+ output_idx += 1;
+ output[output_idx] = (third << 6) | base64_fourth.to_u8();
+ output_idx += 1;
+ chunks = rest;
+ } else {
+ return Err(DecodeErr::InvalidByte);
+ }
+ }
+ match rem.len() {
+ 0 => {}
+ 1 => impossible!("there is a bug in base64_pad::trim_padding"),
+ 2 => {
+ if let Some(base64_fst) = Alphabet::from_ascii(rem[0])
+ && let Some(base64_snd) = Alphabet::from_ascii(rem[1])
+ {
+ snd = base64_snd.to_u8();
+ if snd.trailing_zeros() < 4 {
+ cold_path();
+ return Err(DecodeErr::TrailingBits);
+ } else {
+ output[output_idx] = (base64_fst.to_u8() << 2) | (snd >> 4);
+ }
+ } else {
+ cold_path();
+ return Err(DecodeErr::InvalidByte);
+ }
+ }
+ 3 => {
+ if let Some(base64_fst) = Alphabet::from_ascii(rem[0])
+ && let Some(base64_snd) = Alphabet::from_ascii(rem[1])
+ && let Some(base64_third) = Alphabet::from_ascii(rem[2])
+ {
+ (snd, third) = (base64_snd.to_u8(), base64_third.to_u8());
+ if third.trailing_zeros() < 2 {
+ cold_path();
+ return Err(DecodeErr::TrailingBits);
+ } else {
+ output[output_idx] = (base64_fst.to_u8() << 2) | (snd >> 4);
+ output[output_idx + 1] = (snd << 4) | (third >> 2);
+ }
+ } else {
+ cold_path();
+ return Err(DecodeErr::InvalidByte);
+ }
+ }
+ _ => impossible!("there is a bug in core::slice::as_chunks"),
+ }
+ // SAFETY:
+ // `output.len() >= output_len`.
+ Ok(unsafe { output.split_at_mut_unchecked(output_len) }.0)
+ } else {
+ cold_path();
+ 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 base64_pad::DecodeErr;
+/// assert_eq!(base64_pad::decode([0; 0].as_slice())?, b"");
+/// assert_eq!(
+/// base64_pad::decode(b"A").unwrap_err(),
+/// DecodeErr::EncodedLen
+/// );
+/// assert_eq!(
+/// base64_pad::decode(b"A-==").unwrap_err(),
+/// DecodeErr::InvalidByte
+/// );
+/// assert_eq!(
+/// base64_pad::decode(b"+8==").unwrap_err(),
+/// DecodeErr::TrailingBits
+/// );
+/// assert_eq!(base64_pad::decode(b"C8Aa/A++91VZbx0=")?, [0x0b, 0xc0, 0x1a, 0xfc, 0x0f, 0xbe, 0xf7, b'U', b'Y', b'o', 0x1d]);
+/// # Ok::<_, DecodeErr>(())
+/// ```
+#[cfg(feature = "alloc")]
+#[inline]
+pub fn decode(input: &[u8]) -> Result<Vec<u8>, DecodeErr> {
+ trim_padding(input)
+ .ok_or(DecodeErr::EncodedLen)
+ .and_then(|i| {
+ let capacity = decode_no_pad_len(i.len());
+ 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 base64 with 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 base64 with padding.
+///
+/// Note since no buffer is used to decode the data into, neither [`DecodeErr::BufferLen`] nor
+/// [`DecodeErr::TryReserve`] will ever be returned.
+///
+/// # Examples
+///
+/// ```
+/// # use base64_pad::DecodeErr;
+/// base64_pad::validate_encoded_data(b"")?;
+/// assert_eq!(
+/// base64_pad::validate_encoded_data(b"A").unwrap_err(),
+/// DecodeErr::EncodedLen
+/// );
+/// assert_eq!(
+/// base64_pad::validate_encoded_data(b"A_==").unwrap_err(),
+/// DecodeErr::InvalidByte
+/// );
+/// assert_eq!(
+/// base64_pad::validate_encoded_data(b"+8==").unwrap_err(),
+/// DecodeErr::TrailingBits
+/// );
+/// base64_pad::validate_encoded_data(b"C8Aa/A++91VZbx0=")?;
+/// # Ok::<_, DecodeErr>(())
+/// ```
+#[expect(
+ clippy::missing_asserts_for_indexing,
+ reason = "trust the compiler to already optimize since we match on the length"
+)]
+#[expect(clippy::indexing_slicing, reason = "comments justify correctness")]
+#[inline]
+pub const fn validate_encoded_data(input: &[u8]) -> Result<(), DecodeErr> {
+ if let Some(i) = trim_padding(input) {
+ let (mut chunks, rem) = i.as_chunks::<4>();
+ // There is a _substantial_ boost in performance if we chunk decode.
+ while let [first, ref rest @ ..] = *chunks {
+ if Alphabet::from_ascii(first[0]).is_some()
+ && Alphabet::from_ascii(first[1]).is_some()
+ && Alphabet::from_ascii(first[2]).is_some()
+ && Alphabet::from_ascii(first[3]).is_some()
+ {
+ chunks = rest;
+ } else {
+ return Err(DecodeErr::InvalidByte);
+ }
+ }
+ match rem.len() {
+ 0 => Ok(()),
+ 1 => impossible!("there is a bug in base64_pad::trim_padding"),
+ 2 => {
+ if Alphabet::from_ascii(rem[0]).is_some()
+ && let Some(base64_snd) = Alphabet::from_ascii(rem[1])
+ {
+ if base64_snd.to_u8().trailing_zeros() < 4 {
+ cold_path();
+ Err(DecodeErr::TrailingBits)
+ } else {
+ Ok(())
+ }
+ } else {
+ cold_path();
+ Err(DecodeErr::InvalidByte)
+ }
+ }
+ 3 => {
+ if Alphabet::from_ascii(rem[0]).is_some()
+ && Alphabet::from_ascii(rem[1]).is_some()
+ && let Some(base64_third) = Alphabet::from_ascii(rem[2])
+ {
+ if base64_third.to_u8().trailing_zeros() < 2 {
+ cold_path();
+ Err(DecodeErr::TrailingBits)
+ } else {
+ Ok(())
+ }
+ } else {
+ cold_path();
+ Err(DecodeErr::InvalidByte)
+ }
+ }
+ _ => impossible!("there is a bug in core::slice::as_chunks"),
+ }
+ } else {
+ Err(DecodeErr::EncodedLen)
+ }
+}
+/// 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];
+/// base64_pad::encode_buffer_exact([0; 0].as_slice(), &mut buffer[..0]);
+/// base64_pad::encode_buffer_exact([0; 1].as_slice(), &mut buffer[..4]);
+/// assert_eq!(*b"AA==", buffer[..4]);
+/// // Uncommenting below will cause a `panic` since the output buffer must be exact.
+/// // base64_pad::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.
+///
+/// Note one must be careful in determining the length of `output` before calling; in particular, one can't simply
+/// call [`decode_len`] on the length of `input` since `input` may contain padding which will cause `decode_len`
+/// to return a value that is larger than necessary.
+///
+/// # 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 and `input`
+/// has a valid encoded length (i.e., [`DecodeErr::EncodedLen`] is returned _not_ a `panic` when `input`
+/// has invalid length).
+///
+/// # Examples
+///
+/// ```
+/// # use base64_pad::DecodeErr;
+/// assert_eq!(
+/// base64_pad::decode_buffer_exact(b"A", [0; 0].as_mut_slice()).unwrap_err(),
+/// DecodeErr::EncodedLen
+/// );
+/// assert_eq!(
+/// base64_pad::decode_buffer_exact(b"-A==", [0; 1].as_mut_slice()).unwrap_err(),
+/// DecodeErr::InvalidByte
+/// );
+/// assert_eq!(
+/// base64_pad::decode_buffer_exact(b"+8==", [0; 1].as_mut_slice()).unwrap_err(),
+/// DecodeErr::TrailingBits
+/// );
+/// let mut buffer = [0; base64_pad::decode_len(b"C8Aa/A++91VZbx0=".len()).unwrap() - 1];
+/// base64_pad::decode_buffer_exact(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.
+/// // base64_pad::decode_buffer_exact(
+/// // b"C8Aa/A++91VZbx0=",
+/// // &mut [0; base64_pad::decode_len(b"C8Aa/A++91VZbx0=".len()).unwrap()],
+/// // )?;
+/// # Ok::<_, DecodeErr>(())
+/// ```
+#[expect(
+ clippy::panic,
+ 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> {
+ let output_len = output.len();
+ match decode_buffer(input, output) {
+ Ok(v) => {
+ assert!(
+ v.len() == output_len,
+ "decode_buffer_exact must be passed an output buffer whose length is exactly the length needed to decode the data"
+ );
+ Ok(())
+ }
+ Err(e) => {
+ if matches!(e, DecodeErr::BufferLen) {
+ cold_path();
+ panic!(
+ "decode_buffer_exact must be passed an output buffer whose length is exactly the length needed to decode the data"
+ );
+ } else {
+ Err(e)
+ }
+ }
+ }
+}
diff --git a/src/tests.rs b/src/tests.rs
@@ -0,0 +1,275 @@
+use super::DecodeErr;
+#[cfg(any(
+ target_pointer_width = "16",
+ target_pointer_width = "32",
+ target_pointer_width = "64",
+))]
+use super::MAX_ENCODE_INPUT_LEN;
+#[cfg(feature = "alloc")]
+use alloc::{borrow::ToOwned as _, string::String, vec};
+use core::fmt::{self, Write};
+#[cfg(any(
+ target_pointer_width = "16",
+ target_pointer_width = "32",
+ target_pointer_width = "64",
+))]
+use rand::{RngExt as _, rngs::SmallRng};
+#[expect(clippy::panic, reason = "want to crash when there is a bug")]
+#[expect(clippy::too_many_lines, reason = "a lot to test")]
+#[expect(
+ clippy::as_conversions,
+ clippy::cast_possible_truncation,
+ clippy::integer_division_remainder_used,
+ reason = "comment justifies correctness"
+)]
+#[cfg(any(
+ target_pointer_width = "16",
+ target_pointer_width = "32",
+ target_pointer_width = "64",
+))]
+#[ignore = "slow"]
+#[test]
+fn encode_decode_len() {
+ assert_eq!(MAX_ENCODE_INPUT_LEN, 3 * (usize::MAX >> 2u8));
+ let mut rng = rand::make_rng::<SmallRng>();
+ for _ in 0u32..10_000_000 {
+ // `uN as usize` is fine since we `cfg` by pointer width.
+ #[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_eq!(
+ super::encode_len_checked(len).map(|l| {
+ super::decode_len(l).map(|decode_len| {
+ decode_len
+ - match len % 3 {
+ 0 => 0,
+ 1 => 2,
+ 2 => 1,
+ _ => panic!("usize % 3 is greater than 3"),
+ }
+ })
+ }),
+ Some(Some(len))
+ );
+ } else {
+ assert!(super::encode_len_checked(len).is_none());
+ }
+ }
+ for i in 0..1025 {
+ assert_eq!(
+ super::encode_len_checked(i).map(|l| {
+ super::decode_len(l).map(|decode_len| {
+ decode_len
+ - match i % 3 {
+ 0 => 0,
+ 1 => 2,
+ 2 => 1,
+ _ => panic!("usize % 3 is greater than 3"),
+ }
+ })
+ }),
+ Some(Some(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_eq!(
+ super::encode_len_checked(MAX_ENCODE_INPUT_LEN),
+ Some(usize::MAX - 3)
+ );
+ for _ in 0u32..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.trailing_zeros() > 1 {
+ assert_eq!(
+ super::decode_len(len).map(super::encode_len_checked),
+ Some(Some(len))
+ );
+ } else {
+ assert!(super::decode_len(len).is_none());
+ }
+ }
+ for i in 0usize..1025 {
+ if i.trailing_zeros() > 1 {
+ assert_eq!(
+ super::decode_len(i).map(super::encode_len_checked),
+ Some(Some(i))
+ );
+ } else {
+ assert!(super::decode_len(i).is_none());
+ }
+ }
+ #[cfg(target_pointer_width = "16")]
+ for i in 0..=usize::MAX {
+ if i & 3 == 1 {
+ assert!(super::decode_len(i).is_none());
+ } else {
+ assert_eq!(
+ super::decode_len(i).map(super::encode_len_checked),
+ Some(Some(i))
+ );
+ }
+ }
+ #[cfg(not(target_pointer_width = "16"))]
+ for i in usize::MAX - 1_000_000..=usize::MAX {
+ if i.trailing_zeros() > 1 {
+ assert_eq!(
+ super::decode_len(i).map(super::encode_len_checked),
+ Some(Some(i))
+ );
+ } else {
+ assert!(super::decode_len(i).is_none());
+ }
+ }
+ assert_eq!(
+ super::decode_len(usize::MAX - 3),
+ Some(MAX_ENCODE_INPUT_LEN)
+ );
+}
+#[expect(clippy::indexing_slicing, reason = "comments justify correctness")]
+#[cfg(feature = "alloc")]
+#[test]
+fn encode_write() {
+ let input = [9; 8192];
+ let mut buffer = String::with_capacity(super::encode_len(input.len()));
+ let cap = buffer.capacity();
+ let mut write_len;
+ for len in 0..input.len() {
+ write_len = super::encode_len(len);
+ match write_len.checked_add(buffer.len()) {
+ None => {
+ buffer.clear();
+ // Indexing is fine since `len <= input.len()`.
+ assert_eq!(super::encode_write(&input[..len], &mut buffer), Ok(()));
+ assert_eq!(buffer.len(), write_len);
+ }
+ Some(l) => {
+ if l > cap {
+ buffer.clear();
+ // Indexing is fine since `len <= input.len()`.
+ assert_eq!(super::encode_write(&input[..len], &mut buffer), Ok(()));
+ assert_eq!(buffer.len(), write_len);
+ } else {
+ // Indexing is fine since `len <= input.len()`.
+ assert_eq!(super::encode_write(&input[..len], &mut buffer), Ok(()));
+ assert_eq!(buffer.len(), l);
+ }
+ }
+ }
+ assert!(
+ buffer
+ .as_bytes()
+ .iter()
+ .all(|b| { matches!(*b, b'C' | b'J' | b'Q' | b'k' | b'=') })
+ );
+ }
+}
+#[cfg(feature = "alloc")]
+#[test]
+fn try_encode() {
+ assert_eq!(super::try_encode(&[0]), Ok("AA==".to_owned()));
+}
+#[cfg(feature = "alloc")]
+#[test]
+fn encode() {
+ assert_eq!(super::encode(&[0]), "AA==".to_owned());
+}
+#[cfg(feature = "alloc")]
+#[test]
+fn try_encode_append() {
+ let mut val = "Hi".to_owned();
+ assert_eq!(
+ super::try_encode_append(&[0], &mut val),
+ Ok("AA==".to_owned().as_mut_str())
+ );
+ assert_eq!(val, "HiAA==");
+}
+#[cfg(feature = "alloc")]
+#[test]
+fn encode_append() {
+ let mut val = "Hi".to_owned();
+ assert_eq!(
+ super::encode_append(&[0], &mut val),
+ "AA==".to_owned().as_mut_str()
+ );
+ assert_eq!(val, "HiAA==");
+}
+#[cfg(feature = "alloc")]
+#[test]
+fn try_encode_append_only() {
+ let mut val = "Hi".to_owned();
+ assert_eq!(super::try_encode_append_only(&[0], &mut val), Ok(()));
+ assert_eq!(val, "HiAA==");
+}
+#[cfg(feature = "alloc")]
+#[test]
+fn encode_append_only() {
+ let mut val = "Hi".to_owned();
+ super::encode_append_only(&[0], &mut val);
+ assert_eq!(val, "HiAA==");
+}
+#[test]
+fn decode_buffer() {
+ assert_eq!(
+ super::decode_buffer(b"AA==", &mut [0]),
+ Ok([0].as_mut_slice())
+ );
+ assert_eq!(
+ super::decode_buffer(b"AA==", &mut []),
+ Err(DecodeErr::BufferLen)
+ );
+}
+#[cfg(feature = "alloc")]
+#[test]
+fn decode() {
+ assert_eq!(super::decode(b"AA=="), Ok(vec![0]));
+}
+#[test]
+fn validate_encoded_data() {
+ assert_eq!(super::validate_encoded_data(b"AA=="), Ok(()));
+}
+#[test]
+fn encode_buffer_exact() {
+ let mut output = [0; 4];
+ super::encode_buffer_exact(&[0], &mut output);
+ assert_eq!(output, *b"AA==");
+}
+#[test]
+fn decode_buffer_exact() {
+ let mut output = [1];
+ assert_eq!(super::decode_buffer_exact(b"AA==", &mut output), Ok(()));
+ assert_eq!(output, [0]);
+}
+#[test]
+fn encode_write_core() {
+ struct Writer<'a>(&'a mut [u8]);
+ impl Write for Writer<'_> {
+ fn write_str(&mut self, s: &str) -> fmt::Result {
+ self.0.get_mut(..s.len()).ok_or(fmt::Error).map(|bytes| {
+ bytes.copy_from_slice(s.as_bytes());
+ })
+ }
+ }
+ let mut writer = Writer(&mut [0, 0, 0, 0, 1]);
+ assert_eq!(super::encode_write(&[0], &mut writer), Ok(()));
+ assert_eq!(writer.0, b"AA==\x01");
+ assert_eq!(
+ super::encode_write(&[0, 0, 0, 0], &mut writer),
+ Err(fmt::Error)
+ );
+}