base64_pad

base64 with padding library.
git clone https://git.philomathiclife.com/repos/base64_pad
Log | Files | Refs | README

commit 5faebf1a905c3acb2ad545e183b631d91e6d3d2c
Author: Zack Newman <zack@philomathiclife.com>
Date:   Sun, 12 Jul 2026 13:54:19 -0600

init

Diffstat:
A.gitignore | 2++
ACargo.toml | 123+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ALICENSE-APACHE | 177+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ALICENSE-MIT | 20++++++++++++++++++++
AREADME.md | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib.rs | 1860+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/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)&ensp;[![crates-io]](https://crates.io/crates/base64_pad)&ensp;[![docs-rs]](crate) +//! +//! [git]: https://git.philomathiclife.com/git_badge.svg +//! [crates-io]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust +//! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs +//! +//! `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) + ); +}