Merge "Add OWNERS" into sc-mainline-prod
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json
new file mode 100644
index 0000000..f6e5649
--- /dev/null
+++ b/.cargo_vcs_info.json
@@ -0,0 +1,5 @@
+{
+  "git": {
+    "sha1": "b4fc91325ec985e2a18e83e95a3c08eebd636af4"
+  }
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..bca35b1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,14 @@
+target/
+Cargo.lock
+*~
+*.swp
+*.swo
+main.rs
+
+# JetBrains tools
+.idea
+*.iml
+
+# `perf record` files
+perf.data*
+/tmp
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..fc3ec8f
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,50 @@
+---
+language: rust
+dist: trusty
+sudo: required
+
+matrix:
+  include:
+    - rust: 1.34.0
+    # cfg(doctest) is experimental in 1.39 but ignored with 1.34.0, and that snuck in when 1.39.0 wasn't tested
+    - rust: 1.39.0
+    - rust: stable
+    - rust: beta
+    - rust: nightly
+      addons:
+        apt:
+          packages:
+            # cargo-tarpaulin needs this
+            - libssl-dev
+      install:
+        # For test coverage. In install step so that it can use cache.
+        - cargo tarpaulin --version || RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install --force cargo-tarpaulin
+        - cargo +nightly install cargo-fuzz
+
+    # no_std
+    - rust: stable
+      env: TARGET="--target thumbv6m-none-eabi" FEATURES="--no-default-features --features alloc"
+      install:
+        - rustup target add thumbv6m-none-eabi
+
+cache: cargo
+
+env:
+  # prevent cargo fuzz list from printing with color
+  - TERM=dumb
+
+script:
+  - cargo build --verbose $TARGET --no-default-features 
+  - cargo build --verbose $TARGET $FEATURES
+  - 'if [[ -z "$TARGET" ]]; then cargo test --verbose; fi'
+  - 'if [[ -z "$TARGET" ]]; then cargo doc --verbose; fi'
+  - 'if [[ "$TRAVIS_RUST_VERSION" = nightly ]]; then cargo bench --no-run; fi'
+  # run for just a second to confirm that it can build and run ok
+  - 'if [[ "$TRAVIS_RUST_VERSION" = nightly ]]; then cargo fuzz list | xargs -L 1 -I FUZZER cargo fuzz run FUZZER -- -max_total_time=1; fi'
+
+after_success: |
+  if [[ "$TRAVIS_RUST_VERSION" = nightly ]]; then
+    # Calculate test coverage
+    cargo tarpaulin --out Xml
+    bash <(curl -s https://codecov.io/bash)
+  fi
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..5c43223
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,86 @@
+// This file is generated by cargo2android.py --config cargo2android.json.
+// Do not modify this file as changes will be overridden on upgrade.
+
+
+
+rust_library {
+    name: "libbase64_rust",
+    stem: "libbase64",
+    host_supported: true,
+    crate_name: "base64",
+    srcs: ["src/lib.rs"],
+    edition: "2018",
+    features: [
+        "default",
+        "std",
+    ],
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.resolv",
+    ],
+    min_sdk_version: "29",
+}
+
+rust_defaults {
+    name: "base64_defaults_base64",
+    crate_name: "base64",
+    test_suites: ["general-tests"],
+    auto_gen_config: true,
+    edition: "2018",
+    features: [
+        "default",
+        "std",
+    ],
+    rustlibs: [
+        "libbase64_rust",
+        "libcriterion",
+        "librand",
+        "libstructopt",
+    ],
+}
+
+rust_test_host {
+    name: "base64_host_test_tests_decode",
+    defaults: ["base64_defaults_base64"],
+    srcs: ["tests/decode.rs"],
+    test_options: {
+        unit_test: true,
+    },
+}
+
+rust_test {
+    name: "base64_device_test_tests_decode",
+    defaults: ["base64_defaults_base64"],
+    srcs: ["tests/decode.rs"],
+}
+
+rust_test_host {
+    name: "base64_host_test_tests_encode",
+    defaults: ["base64_defaults_base64"],
+    srcs: ["tests/encode.rs"],
+    test_options: {
+        unit_test: true,
+    },
+}
+
+rust_test {
+    name: "base64_device_test_tests_encode",
+    defaults: ["base64_defaults_base64"],
+    srcs: ["tests/encode.rs"],
+}
+
+rust_test_host {
+    name: "base64_host_test_tests_helpers",
+    defaults: ["base64_defaults_base64"],
+    srcs: ["tests/helpers.rs"],
+    test_options: {
+        unit_test: true,
+    },
+}
+
+rust_test {
+    name: "base64_device_test_tests_helpers",
+    defaults: ["base64_defaults_base64"],
+    srcs: ["tests/helpers.rs"],
+}
+
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..2fd8878
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,43 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies
+#
+# If you believe there's an error in this file please file an
+# issue against the rust-lang/cargo repository. If you're
+# editing this file be aware that the upstream Cargo.toml
+# will likely look very different (and much more reasonable)
+
+[package]
+edition = "2018"
+name = "base64"
+version = "0.13.0"
+authors = ["Alice Maz <alice@alicemaz.com>", "Marshall Pierce <marshall@mpierce.org>"]
+description = "encodes and decodes base64 as bytes or utf8"
+documentation = "https://docs.rs/base64"
+readme = "README.md"
+keywords = ["base64", "utf8", "encode", "decode", "no_std"]
+categories = ["encoding"]
+license = "MIT/Apache-2.0"
+repository = "https://github.com/marshallpierce/rust-base64"
+[profile.bench]
+debug = true
+
+[[bench]]
+name = "benchmarks"
+harness = false
+[dev-dependencies.criterion]
+version = "=0.3.2"
+
+[dev-dependencies.rand]
+version = "0.6.1"
+
+[dev-dependencies.structopt]
+version = "0.3"
+
+[features]
+alloc = []
+default = ["std"]
+std = []
diff --git a/Cargo.toml.orig b/Cargo.toml.orig
new file mode 100644
index 0000000..30e73ee
--- /dev/null
+++ b/Cargo.toml.orig
@@ -0,0 +1,31 @@
+[package]
+name = "base64"
+version = "0.13.0"
+authors = ["Alice Maz <alice@alicemaz.com>", "Marshall Pierce <marshall@mpierce.org>"]
+description = "encodes and decodes base64 as bytes or utf8"
+repository = "https://github.com/marshallpierce/rust-base64"
+documentation = "https://docs.rs/base64"
+readme = "README.md"
+keywords = ["base64", "utf8", "encode", "decode", "no_std"]
+categories = ["encoding"]
+license = "MIT/Apache-2.0"
+edition = "2018"
+
+[[bench]]
+name = "benchmarks"
+harness = false
+
+[dev-dependencies]
+# 0.3.3 requires rust 1.36.0 for stable copied()
+criterion = "=0.3.2"
+rand = "0.6.1"
+structopt = "0.3"
+
+[features]
+default = ["std"]
+alloc = []
+std = []
+
+[profile.bench]
+# Useful for better disassembly when using `perf record` and `perf annotate`
+debug = true
diff --git a/LICENSE b/LICENSE
new file mode 120000
index 0000000..6b579aa
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1 @@
+LICENSE-APACHE
\ No newline at end of file
diff --git a/LICENSE-APACHE b/LICENSE-APACHE
new file mode 100644
index 0000000..16fe87b
--- /dev/null
+++ b/LICENSE-APACHE
@@ -0,0 +1,201 @@
+                              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
+
+APPENDIX: How to apply the Apache License to your work.
+
+   To apply the Apache License to your work, attach the following
+   boilerplate notice, with the fields enclosed by brackets "[]"
+   replaced with your own identifying information. (Don't include
+   the brackets!)  The text should be enclosed in the appropriate
+   comment syntax for the file format. We also recommend that a
+   file or class name and description of purpose be included on the
+   same "printed page" as the copyright notice for easier
+   identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/LICENSE-MIT b/LICENSE-MIT
new file mode 100644
index 0000000..7bc10f8
--- /dev/null
+++ b/LICENSE-MIT
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Alice Maz
+
+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/METADATA b/METADATA
new file mode 100644
index 0000000..c32db9c
--- /dev/null
+++ b/METADATA
@@ -0,0 +1,20 @@
+name: "base64"
+description: "encodes and decodes base64 as bytes or utf8"
+third_party {
+  url {
+    type: HOMEPAGE
+    value: "https://crates.io/crates/base64"
+  }
+  url {
+    type: ARCHIVE
+    value: "https://static.crates.io/crates/base64/base64-0.13.0.crate"
+  }
+  version: "0.13.0"
+  # Dual-licensed, using the least restrictive per go/thirdpartylicenses#same.
+  license_type: NOTICE
+  last_upgrade_date {
+    year: 2021
+    month: 6
+    day: 23
+  }
+}
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7815966
--- /dev/null
+++ b/README.md
@@ -0,0 +1,114 @@
+[base64](https://crates.io/crates/base64)
+===
+
+[![](https://img.shields.io/crates/v/base64.svg)](https://crates.io/crates/base64) [![Docs](https://docs.rs/base64/badge.svg)](https://docs.rs/base64) [![Build](https://travis-ci.org/marshallpierce/rust-base64.svg?branch=master)](https://travis-ci.org/marshallpierce/rust-base64) [![codecov](https://codecov.io/gh/marshallpierce/rust-base64/branch/master/graph/badge.svg)](https://codecov.io/gh/marshallpierce/rust-base64) [![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/)
+
+<a href="https://www.jetbrains.com/?from=rust-base64"><img src="/icon_CLion.svg" height="40px"/></a>
+
+Made with CLion. Thanks to JetBrains for supporting open source!
+
+It's base64. What more could anyone want?
+
+This library's goals are to be *correct* and *fast*. It's thoroughly tested and widely used. It exposes functionality at multiple levels of abstraction so you can choose the level of convenience vs performance that you want, e.g. `decode_config_slice` decodes into an existing `&mut [u8]` and is pretty fast (2.6GiB/s for a 3 KiB input), whereas `decode_config` allocates a new `Vec<u8>` and returns it, which might be more convenient in some cases, but is slower (although still fast enough for almost any purpose) at 2.1 GiB/s.
+
+Example
+---
+
+```rust
+extern crate base64;
+
+use base64::{encode, decode};
+
+fn main() {
+    let a = b"hello world";
+    let b = "aGVsbG8gd29ybGQ=";
+
+    assert_eq!(encode(a), b);
+    assert_eq!(a, &decode(b).unwrap()[..]);
+}
+```
+
+See the [docs](https://docs.rs/base64) for all the details.
+
+Rust version compatibility
+---
+
+The minimum required Rust version is 1.34.0.
+
+Developing
+---
+
+Benchmarks are in `benches/`. Running them requires nightly rust, but `rustup` makes it easy:
+
+```bash
+rustup run nightly cargo bench
+```
+
+Decoding is aided by some pre-calculated tables, which are generated by:
+
+```bash
+cargo run --example make_tables > src/tables.rs.tmp && mv src/tables.rs.tmp src/tables.rs
+```
+
+no_std
+---
+
+This crate supports no_std. By default the crate targets std via the `std` feature. You can deactivate the `default-features` to target core instead. In that case you lose out on all the functionality revolving around `std::io`, `std::error::Error` and heap allocations. There is an additional `alloc` feature that you can activate to bring back the support for heap allocations.
+
+Profiling
+---
+
+On Linux, you can use [perf](https://perf.wiki.kernel.org/index.php/Main_Page) for profiling. Then compile the benchmarks with `rustup nightly run cargo bench --no-run`.
+
+Run the benchmark binary with `perf` (shown here filtering to one particular benchmark, which will make the results easier to read). `perf` is only available to the root user on most systems as it fiddles with event counters in your CPU, so use `sudo`. We need to run the actual benchmark binary, hence the path into `target`. You can see the actual full path with `rustup run nightly cargo bench -v`; it will print out the commands it runs. If you use the exact path that `bench` outputs, make sure you get the one that's for the benchmarks, not the tests. You may also want to `cargo clean` so you have only one `benchmarks-` binary (they tend to accumulate).
+
+```bash
+sudo perf record target/release/deps/benchmarks-* --bench decode_10mib_reuse
+```
+
+Then analyze the results, again with perf:
+
+```bash
+sudo perf annotate -l
+```
+
+You'll see a bunch of interleaved rust source and assembly like this. The section with `lib.rs:327` is telling us that 4.02% of samples saw the `movzbl` aka bit shift as the active instruction. However, this percentage is not as exact as it seems due to a phenomenon called *skid*. Basically, a consequence of how fancy modern CPUs are is that this sort of instruction profiling is inherently inaccurate, especially in branch-heavy code.
+
+```text
+ lib.rs:322    0.70 :     10698:       mov    %rdi,%rax
+    2.82 :        1069b:       shr    $0x38,%rax
+         :                  if morsel == decode_tables::INVALID_VALUE {
+         :                      bad_byte_index = input_index;
+         :                      break;
+         :                  };
+         :                  accum = (morsel as u64) << 58;
+ lib.rs:327    4.02 :     1069f:       movzbl (%r9,%rax,1),%r15d
+         :              // fast loop of 8 bytes at a time
+         :              while input_index < length_of_full_chunks {
+         :                  let mut accum: u64;
+         :
+         :                  let input_chunk = BigEndian::read_u64(&input_bytes[input_index..(input_index + 8)]);
+         :                  morsel = decode_table[(input_chunk >> 56) as usize];
+ lib.rs:322    3.68 :     106a4:       cmp    $0xff,%r15
+         :                  if morsel == decode_tables::INVALID_VALUE {
+    0.00 :        106ab:       je     1090e <base64::decode_config_buf::hbf68a45fefa299c1+0x46e>
+```
+
+
+Fuzzing
+---
+
+This uses [cargo-fuzz](https://github.com/rust-fuzz/cargo-fuzz). See `fuzz/fuzzers` for the available fuzzing scripts. To run, use an invocation like these:
+
+```bash
+cargo +nightly fuzz run roundtrip
+cargo +nightly fuzz run roundtrip_no_pad
+cargo +nightly fuzz run roundtrip_random_config -- -max_len=10240
+cargo +nightly fuzz run decode_random
+```
+
+
+License
+---
+
+This project is dual-licensed under MIT and Apache 2.0.
diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
new file mode 100644
index 0000000..1048c1e
--- /dev/null
+++ b/RELEASE-NOTES.md
@@ -0,0 +1,105 @@
+# 0.13.0
+
+- Config methods are const
+- Added `EncoderStringWriter` to allow encoding directly to a String
+- `EncoderWriter` now owns its delegate writer rather than keeping a reference to it (though refs still work)
+    - As a consequence, it is now possible to extract the delegate writer from an `EncoderWriter` via `finish()`, which returns `Result<W>` instead of `Result<()>`. If you were calling `finish()` explicitly, you will now need to use `let _ = foo.finish()` instead of just `foo.finish()` to avoid a warning about the unused value.
+- When decoding input that has both an invalid length and an invalid symbol as the last byte, `InvalidByte` will be emitted instead of `InvalidLength` to make the problem more obvious.
+
+# 0.12.2
+
+- Add `BinHex` alphabet
+
+# 0.12.1
+
+- Add `Bcrypt` alphabet
+
+# 0.12.0
+
+- A `Read` implementation (`DecoderReader`) to let users transparently decoded data from a b64 input source
+- IMAP's modified b64 alphabet
+- Relaxed type restrictions to just `AsRef<[ut8]>` for main `encode*`/`decode*` functions
+- A minor performance improvement in encoding
+
+# 0.11.0
+- Minimum rust version 1.34.0
+- `no_std` is now supported via the two new features `alloc` and `std`.
+
+# 0.10.1
+
+- Minimum rust version 1.27.2
+- Fix bug in streaming encoding ([#90](https://github.com/marshallpierce/rust-base64/pull/90)): if the underlying writer didn't write all the bytes given to it, the remaining bytes would not be retried later. See the docs on `EncoderWriter::write`.
+- Make it configurable whether or not to return an error when decoding detects excess trailing bits.
+
+# 0.10.0
+
+- Remove line wrapping. Line wrapping was never a great conceptual fit in this library, and other features (streaming encoding, etc) either couldn't support it or could support only special cases of it with a great increase in complexity. Line wrapping has been pulled out into a [line-wrap](https://crates.io/crates/line-wrap) crate, so it's still available if you need it.
+  - `Base64Display` creation no longer uses a `Result` because it can't fail, which means its helper methods for common
+  configs that `unwrap()` for you are no longer needed
+- Add a streaming encoder `Write` impl to transparently base64 as you write.
+- Remove the remaining `unsafe` code.
+- Remove whitespace stripping to simplify `no_std` support. No out of the box configs use it, and it's trivial to do yourself if needed: `filter(|b| !b" \n\t\r\x0b\x0c".contains(b)`.
+- Detect invalid trailing symbols when decoding and return an error rather than silently ignoring them.
+
+# 0.9.3
+
+- Update safemem
+
+# 0.9.2
+
+- Derive `Clone` for `DecodeError`.
+
+# 0.9.1
+
+- Add support for `crypt(3)`'s base64 variant.
+
+# 0.9.0
+
+- `decode_config_slice` function for no-allocation decoding, analogous to `encode_config_slice`
+- Decode performance optimization
+
+# 0.8.0
+
+- `encode_config_slice` function for no-allocation encoding
+
+# 0.7.0
+
+- `STANDARD_NO_PAD` config
+- `Base64Display` heap-free wrapper for use in format strings, etc
+
+# 0.6.0
+
+- Decode performance improvements
+- Use `unsafe` in fewer places
+- Added fuzzers
+
+# 0.5.2
+
+- Avoid usize overflow when calculating length
+- Better line wrapping performance
+
+# 0.5.1
+
+- Temporarily disable line wrapping
+- Add Apache 2.0 license
+
+# 0.5.0
+
+- MIME support, including configurable line endings and line wrapping
+- Removed `decode_ws`
+- Renamed `Base64Error` to `DecodeError`
+
+# 0.4.1
+
+- Allow decoding a `AsRef<[u8]>` instead of just a `&str`
+
+# 0.4.0
+
+- Configurable padding
+- Encode performance improvements
+
+# 0.3.0
+
+- Added encode/decode functions that do not allocate their own storage
+- Decode performance improvements
+- Extraneous padding bytes are no longer ignored. Now, an error will be returned.
diff --git a/TEST_MAPPING b/TEST_MAPPING
new file mode 100644
index 0000000..768046f
--- /dev/null
+++ b/TEST_MAPPING
@@ -0,0 +1,14 @@
+// Generated by update_crate_tests.py for tests that depend on this crate.
+{
+  "presubmit": [
+    {
+      "name": "base64_device_test_tests_decode"
+    },
+    {
+      "name": "base64_device_test_tests_encode"
+    },
+    {
+      "name": "base64_device_test_tests_helpers"
+    }
+  ]
+}
diff --git a/benches/benchmarks.rs b/benches/benchmarks.rs
new file mode 100644
index 0000000..ddcb734
--- /dev/null
+++ b/benches/benchmarks.rs
@@ -0,0 +1,210 @@
+extern crate base64;
+#[macro_use]
+extern crate criterion;
+extern crate rand;
+
+use base64::display;
+use base64::{
+    decode, decode_config_buf, decode_config_slice, encode, encode_config_buf, encode_config_slice,
+    write, Config,
+};
+
+use criterion::{black_box, Bencher, Criterion, ParameterizedBenchmark, Throughput};
+use rand::{FromEntropy, Rng};
+use std::io::{self, Read, Write};
+
+const TEST_CONFIG: Config = base64::STANDARD;
+
+fn do_decode_bench(b: &mut Bencher, &size: &usize) {
+    let mut v: Vec<u8> = Vec::with_capacity(size * 3 / 4);
+    fill(&mut v);
+    let encoded = encode(&v);
+
+    b.iter(|| {
+        let orig = decode(&encoded);
+        black_box(&orig);
+    });
+}
+
+fn do_decode_bench_reuse_buf(b: &mut Bencher, &size: &usize) {
+    let mut v: Vec<u8> = Vec::with_capacity(size * 3 / 4);
+    fill(&mut v);
+    let encoded = encode(&v);
+
+    let mut buf = Vec::new();
+    b.iter(|| {
+        decode_config_buf(&encoded, TEST_CONFIG, &mut buf).unwrap();
+        black_box(&buf);
+        buf.clear();
+    });
+}
+
+fn do_decode_bench_slice(b: &mut Bencher, &size: &usize) {
+    let mut v: Vec<u8> = Vec::with_capacity(size * 3 / 4);
+    fill(&mut v);
+    let encoded = encode(&v);
+
+    let mut buf = Vec::new();
+    buf.resize(size, 0);
+    b.iter(|| {
+        decode_config_slice(&encoded, TEST_CONFIG, &mut buf).unwrap();
+        black_box(&buf);
+    });
+}
+
+fn do_decode_bench_stream(b: &mut Bencher, &size: &usize) {
+    let mut v: Vec<u8> = Vec::with_capacity(size * 3 / 4);
+    fill(&mut v);
+    let encoded = encode(&v);
+
+    let mut buf = Vec::new();
+    buf.resize(size, 0);
+    buf.truncate(0);
+
+    b.iter(|| {
+        let mut cursor = io::Cursor::new(&encoded[..]);
+        let mut decoder = base64::read::DecoderReader::new(&mut cursor, TEST_CONFIG);
+        decoder.read_to_end(&mut buf).unwrap();
+        buf.clear();
+        black_box(&buf);
+    });
+}
+
+fn do_encode_bench(b: &mut Bencher, &size: &usize) {
+    let mut v: Vec<u8> = Vec::with_capacity(size);
+    fill(&mut v);
+    b.iter(|| {
+        let e = encode(&v);
+        black_box(&e);
+    });
+}
+
+fn do_encode_bench_display(b: &mut Bencher, &size: &usize) {
+    let mut v: Vec<u8> = Vec::with_capacity(size);
+    fill(&mut v);
+    b.iter(|| {
+        let e = format!("{}", display::Base64Display::with_config(&v, TEST_CONFIG));
+        black_box(&e);
+    });
+}
+
+fn do_encode_bench_reuse_buf(b: &mut Bencher, &size: &usize) {
+    let mut v: Vec<u8> = Vec::with_capacity(size);
+    fill(&mut v);
+    let mut buf = String::new();
+    b.iter(|| {
+        encode_config_buf(&v, TEST_CONFIG, &mut buf);
+        buf.clear();
+    });
+}
+
+fn do_encode_bench_slice(b: &mut Bencher, &size: &usize) {
+    let mut v: Vec<u8> = Vec::with_capacity(size);
+    fill(&mut v);
+    let mut buf = Vec::new();
+    // conservative estimate of encoded size
+    buf.resize(v.len() * 2, 0);
+    b.iter(|| {
+        encode_config_slice(&v, TEST_CONFIG, &mut buf);
+    });
+}
+
+fn do_encode_bench_stream(b: &mut Bencher, &size: &usize) {
+    let mut v: Vec<u8> = Vec::with_capacity(size);
+    fill(&mut v);
+    let mut buf = Vec::new();
+
+    buf.reserve(size * 2);
+    b.iter(|| {
+        buf.clear();
+        let mut stream_enc = write::EncoderWriter::new(&mut buf, TEST_CONFIG);
+        stream_enc.write_all(&v).unwrap();
+        stream_enc.flush().unwrap();
+    });
+}
+
+fn do_encode_bench_string_stream(b: &mut Bencher, &size: &usize) {
+    let mut v: Vec<u8> = Vec::with_capacity(size);
+    fill(&mut v);
+
+    b.iter(|| {
+        let mut stream_enc = write::EncoderStringWriter::new(TEST_CONFIG);
+        stream_enc.write_all(&v).unwrap();
+        stream_enc.flush().unwrap();
+        let _ = stream_enc.into_inner();
+    });
+}
+
+fn do_encode_bench_string_reuse_buf_stream(b: &mut Bencher, &size: &usize) {
+    let mut v: Vec<u8> = Vec::with_capacity(size);
+    fill(&mut v);
+
+    let mut buf = String::new();
+    b.iter(|| {
+        buf.clear();
+        let mut stream_enc = write::EncoderStringWriter::from(&mut buf, TEST_CONFIG);
+        stream_enc.write_all(&v).unwrap();
+        stream_enc.flush().unwrap();
+        let _ = stream_enc.into_inner();
+    });
+}
+
+fn fill(v: &mut Vec<u8>) {
+    let cap = v.capacity();
+    // weak randomness is plenty; we just want to not be completely friendly to the branch predictor
+    let mut r = rand::rngs::SmallRng::from_entropy();
+    while v.len() < cap {
+        v.push(r.gen::<u8>());
+    }
+}
+
+const BYTE_SIZES: [usize; 5] = [3, 50, 100, 500, 3 * 1024];
+
+// Benchmarks over these byte sizes take longer so we will run fewer samples to
+// keep the benchmark runtime reasonable.
+const LARGE_BYTE_SIZES: [usize; 3] = [3 * 1024 * 1024, 10 * 1024 * 1024, 30 * 1024 * 1024];
+
+fn encode_benchmarks(byte_sizes: &[usize]) -> ParameterizedBenchmark<usize> {
+    ParameterizedBenchmark::new("encode", do_encode_bench, byte_sizes.iter().cloned())
+        .warm_up_time(std::time::Duration::from_millis(500))
+        .measurement_time(std::time::Duration::from_secs(3))
+        .throughput(|s| Throughput::Bytes(*s as u64))
+        .with_function("encode_display", do_encode_bench_display)
+        .with_function("encode_reuse_buf", do_encode_bench_reuse_buf)
+        .with_function("encode_slice", do_encode_bench_slice)
+        .with_function("encode_reuse_buf_stream", do_encode_bench_stream)
+        .with_function("encode_string_stream", do_encode_bench_string_stream)
+        .with_function(
+            "encode_string_reuse_buf_stream",
+            do_encode_bench_string_reuse_buf_stream,
+        )
+}
+
+fn decode_benchmarks(byte_sizes: &[usize]) -> ParameterizedBenchmark<usize> {
+    ParameterizedBenchmark::new("decode", do_decode_bench, byte_sizes.iter().cloned())
+        .warm_up_time(std::time::Duration::from_millis(500))
+        .measurement_time(std::time::Duration::from_secs(3))
+        .throughput(|s| Throughput::Bytes(*s as u64))
+        .with_function("decode_reuse_buf", do_decode_bench_reuse_buf)
+        .with_function("decode_slice", do_decode_bench_slice)
+        .with_function("decode_stream", do_decode_bench_stream)
+}
+
+fn bench(c: &mut Criterion) {
+    c.bench("bench_small_input", encode_benchmarks(&BYTE_SIZES[..]));
+
+    c.bench(
+        "bench_large_input",
+        encode_benchmarks(&LARGE_BYTE_SIZES[..]).sample_size(10),
+    );
+
+    c.bench("bench_small_input", decode_benchmarks(&BYTE_SIZES[..]));
+
+    c.bench(
+        "bench_large_input",
+        decode_benchmarks(&LARGE_BYTE_SIZES[..]).sample_size(10),
+    );
+}
+
+criterion_group!(benches, bench);
+criterion_main!(benches);
diff --git a/cargo2android.json b/cargo2android.json
new file mode 100644
index 0000000..6d85f2f
--- /dev/null
+++ b/cargo2android.json
@@ -0,0 +1,10 @@
+{
+    "add-toplevel-block": "cargo2android_tests.bp",
+    "apex-available": [
+      "//apex_available:platform",
+      "com.android.resolv"
+    ],
+    "min_sdk_version": "29",
+    "device": true,
+    "run": true
+  }
\ No newline at end of file
diff --git a/cargo2android_tests.bp b/cargo2android_tests.bp
new file mode 100644
index 0000000..1213c14
--- /dev/null
+++ b/cargo2android_tests.bp
@@ -0,0 +1,62 @@
+rust_defaults {
+    name: "base64_defaults_base64",
+    crate_name: "base64",
+    test_suites: ["general-tests"],
+    auto_gen_config: true,
+    edition: "2018",
+    features: [
+        "default",
+        "std",
+    ],
+    rustlibs: [
+        "libbase64_rust",
+        "libcriterion",
+        "librand",
+        "libstructopt",
+    ],
+}
+
+rust_test_host {
+    name: "base64_host_test_tests_decode",
+    defaults: ["base64_defaults_base64"],
+    srcs: ["tests/decode.rs"],
+    test_options: {
+        unit_test: true,
+    },
+}
+
+rust_test {
+    name: "base64_device_test_tests_decode",
+    defaults: ["base64_defaults_base64"],
+    srcs: ["tests/decode.rs"],
+}
+
+rust_test_host {
+    name: "base64_host_test_tests_encode",
+    defaults: ["base64_defaults_base64"],
+    srcs: ["tests/encode.rs"],
+    test_options: {
+        unit_test: true,
+    },
+}
+
+rust_test {
+    name: "base64_device_test_tests_encode",
+    defaults: ["base64_defaults_base64"],
+    srcs: ["tests/encode.rs"],
+}
+
+rust_test_host {
+    name: "base64_host_test_tests_helpers",
+    defaults: ["base64_defaults_base64"],
+    srcs: ["tests/helpers.rs"],
+    test_options: {
+        unit_test: true,
+    },
+}
+
+rust_test {
+    name: "base64_device_test_tests_helpers",
+    defaults: ["base64_defaults_base64"],
+    srcs: ["tests/helpers.rs"],
+}
diff --git a/examples/base64.rs b/examples/base64.rs
new file mode 100644
index 0000000..cba745b
--- /dev/null
+++ b/examples/base64.rs
@@ -0,0 +1,89 @@
+use std::fs::File;
+use std::io::{self, Read};
+use std::path::PathBuf;
+use std::process;
+use std::str::FromStr;
+
+use base64::{read, write};
+use structopt::StructOpt;
+
+#[derive(Debug, StructOpt)]
+enum CharacterSet {
+    Standard,
+    UrlSafe,
+}
+
+impl Default for CharacterSet {
+    fn default() -> Self {
+        CharacterSet::Standard
+    }
+}
+
+impl Into<base64::Config> for CharacterSet {
+    fn into(self) -> base64::Config {
+        match self {
+            CharacterSet::Standard => base64::STANDARD,
+            CharacterSet::UrlSafe => base64::URL_SAFE,
+        }
+    }
+}
+
+impl FromStr for CharacterSet {
+    type Err = String;
+    fn from_str(s: &str) -> Result<CharacterSet, String> {
+        match s {
+            "standard" => Ok(CharacterSet::Standard),
+            "urlsafe" => Ok(CharacterSet::UrlSafe),
+            _ => Err(format!("charset '{}' unrecognized", s)),
+        }
+    }
+}
+
+/// Base64 encode or decode FILE (or standard input), to standard output.
+#[derive(Debug, StructOpt)]
+struct Opt {
+    /// decode data
+    #[structopt(short = "d", long = "decode")]
+    decode: bool,
+    /// The character set to choose. Defaults to the standard base64 character set.
+    /// Supported character sets include "standard" and "urlsafe".
+    #[structopt(long = "charset")]
+    charset: Option<CharacterSet>,
+    /// The file to encode/decode.
+    #[structopt(parse(from_os_str))]
+    file: Option<PathBuf>,
+}
+
+fn main() {
+    let opt = Opt::from_args();
+    let stdin;
+    let mut input: Box<dyn Read> = match opt.file {
+        None => {
+            stdin = io::stdin();
+            Box::new(stdin.lock())
+        }
+        Some(ref f) if f.as_os_str() == "-" => {
+            stdin = io::stdin();
+            Box::new(stdin.lock())
+        }
+        Some(f) => Box::new(File::open(f).unwrap()),
+    };
+    let config = opt.charset.unwrap_or_default().into();
+    let stdout = io::stdout();
+    let mut stdout = stdout.lock();
+    let r = if opt.decode {
+        let mut decoder = read::DecoderReader::new(&mut input, config);
+        io::copy(&mut decoder, &mut stdout)
+    } else {
+        let mut encoder = write::EncoderWriter::new(&mut stdout, config);
+        io::copy(&mut input, &mut encoder)
+    };
+    if let Err(e) = r {
+        eprintln!(
+            "Base64 {} failed with {}",
+            if opt.decode { "decode" } else { "encode" },
+            e
+        );
+        process::exit(1);
+    }
+}
diff --git a/examples/make_tables.rs b/examples/make_tables.rs
new file mode 100644
index 0000000..2f27c0e
--- /dev/null
+++ b/examples/make_tables.rs
@@ -0,0 +1,179 @@
+use std::collections::{HashMap, HashSet};
+use std::iter::Iterator;
+
+fn main() {
+    println!("pub const INVALID_VALUE: u8 = 255;");
+
+    // A-Z
+    let standard_alphabet: Vec<u8> = (0x41..0x5B)
+        // a-z
+        .chain(0x61..0x7B)
+        // 0-9
+        .chain(0x30..0x3A)
+        // +
+        .chain(0x2B..0x2C)
+        // /
+        .chain(0x2F..0x30)
+        .collect();
+    print_encode_table(&standard_alphabet, "STANDARD_ENCODE", 0);
+    print_decode_table(&standard_alphabet, "STANDARD_DECODE", 0);
+
+    // A-Z
+    let url_alphabet: Vec<u8> = (0x41..0x5B)
+        // a-z
+        .chain(0x61..0x7B)
+        // 0-9
+        .chain(0x30..0x3A)
+        // -
+        .chain(0x2D..0x2E)
+        // _
+        .chain(0x5F..0x60)
+        .collect();
+    print_encode_table(&url_alphabet, "URL_SAFE_ENCODE", 0);
+    print_decode_table(&url_alphabet, "URL_SAFE_DECODE", 0);
+
+    // ./0123456789
+    let crypt_alphabet: Vec<u8> = (b'.'..(b'9' + 1))
+        // A-Z
+        .chain(b'A'..(b'Z' + 1))
+        // a-z
+        .chain(b'a'..(b'z' + 1))
+        .collect();
+    print_encode_table(&crypt_alphabet, "CRYPT_ENCODE", 0);
+    print_decode_table(&crypt_alphabet, "CRYPT_DECODE", 0);
+
+    // ./
+    let bcrypt_alphabet: Vec<u8> = (b'.'..(b'/' + 1))
+        // A-Z
+        .chain(b'A'..(b'Z' + 1))
+        // a-z
+        .chain(b'a'..(b'z' + 1))
+        // 0-9
+        .chain(b'0'..(b'9' + 1))
+        .collect();
+    print_encode_table(&bcrypt_alphabet, "BCRYPT_ENCODE", 0);
+    print_decode_table(&bcrypt_alphabet, "BCRYPT_DECODE", 0);
+
+    // A-Z
+    let imap_alphabet: Vec<u8> = (0x41..0x5B)
+        // a-z
+        .chain(0x61..0x7B)
+        // 0-9
+        .chain(0x30..0x3A)
+        // +
+        .chain(0x2B..0x2C)
+        // ,
+        .chain(0x2C..0x2D)
+        .collect();
+    print_encode_table(&imap_alphabet, "IMAP_MUTF7_ENCODE", 0);
+    print_decode_table(&imap_alphabet, "IMAP_MUTF7_DECODE", 0);
+
+    // '!' - '-'
+    let binhex_alphabet: Vec<u8> = (0x21..0x2E)
+        // 0-9
+        .chain(0x30..0x3A)
+        // @-N
+        .chain(0x40..0x4F)
+        // P-V
+        .chain(0x50..0x57)
+        // X-[
+        .chain(0x58..0x5C)
+        // `-f
+        .chain(0x60..0x66)
+        // h-m
+        .chain(0x68..0x6E)
+        // p-r
+        .chain(0x70..0x73)
+        .collect();
+    print_encode_table(&binhex_alphabet, "BINHEX_ENCODE", 0);
+    print_decode_table(&binhex_alphabet, "BINHEX_DECODE", 0);
+}
+
+fn print_encode_table(alphabet: &[u8], const_name: &str, indent_depth: usize) {
+    check_alphabet(alphabet);
+    println!("#[rustfmt::skip]");
+    println!(
+        "{:width$}pub const {}: &[u8; 64] = &[",
+        "",
+        const_name,
+        width = indent_depth
+    );
+
+    for (i, b) in alphabet.iter().enumerate() {
+        println!(
+            "{:width$}{}, // input {} (0x{:X}) => '{}' (0x{:X})",
+            "",
+            b,
+            i,
+            i,
+            String::from_utf8(vec![*b as u8]).unwrap(),
+            b,
+            width = indent_depth + 4
+        );
+    }
+
+    println!("{:width$}];", "", width = indent_depth);
+}
+
+fn print_decode_table(alphabet: &[u8], const_name: &str, indent_depth: usize) {
+    check_alphabet(alphabet);
+    // map of alphabet bytes to 6-bit morsels
+    let mut input_to_morsel = HashMap::<u8, u8>::new();
+
+    // standard base64 alphabet bytes, in order
+    for (morsel, ascii_byte) in alphabet.iter().enumerate() {
+        // truncation cast is fine here
+        let _ = input_to_morsel.insert(*ascii_byte, morsel as u8);
+    }
+
+    println!("#[rustfmt::skip]");
+    println!(
+        "{:width$}pub const {}: &[u8; 256] = &[",
+        "",
+        const_name,
+        width = indent_depth
+    );
+    for ascii_byte in 0..256 {
+        let (value, comment) = match input_to_morsel.get(&(ascii_byte as u8)) {
+            None => (
+                "INVALID_VALUE".to_string(),
+                format!("input {} (0x{:X})", ascii_byte, ascii_byte),
+            ),
+            Some(v) => (
+                format!("{}", *v),
+                format!(
+                    "input {} (0x{:X} char '{}') => {} (0x{:X})",
+                    ascii_byte,
+                    ascii_byte,
+                    String::from_utf8(vec![ascii_byte as u8]).unwrap(),
+                    *v,
+                    *v
+                ),
+            ),
+        };
+
+        println!(
+            "{:width$}{}, // {}",
+            "",
+            value,
+            comment,
+            width = indent_depth + 4
+        );
+    }
+    println!("{:width$}];", "", width = indent_depth);
+}
+
+fn check_alphabet(alphabet: &[u8]) {
+    // ensure all characters are distinct
+    assert_eq!(64, alphabet.len());
+    let mut set: HashSet<u8> = HashSet::new();
+    set.extend(alphabet);
+    assert_eq!(64, set.len());
+
+    // must be ASCII to be valid as single UTF-8 bytes
+    for &b in alphabet {
+        assert!(b <= 0x7F_u8);
+        // = is assumed to be padding, so cannot be used as a symbol
+        assert_ne!(b'=', b);
+    }
+}
diff --git a/icon_CLion.svg b/icon_CLion.svg
new file mode 100644
index 0000000..e9edb04
--- /dev/null
+++ b/icon_CLion.svg
@@ -0,0 +1,34 @@
+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 128 128">
+  <defs>
+    <linearGradient id="linear-gradient" x1="40.69" y1="-676.56" x2="83.48" y2="-676.56" gradientTransform="matrix(1, 0, 0, -1, 0, -648.86)" gradientUnits="userSpaceOnUse">
+      <stop offset="0" stop-color="#ed358c"/>
+      <stop offset="0.16" stop-color="#e9388c"/>
+      <stop offset="0.3" stop-color="#de418c"/>
+      <stop offset="0.43" stop-color="#cc508c"/>
+      <stop offset="0.57" stop-color="#b2658d"/>
+      <stop offset="0.7" stop-color="#90808d"/>
+      <stop offset="0.83" stop-color="#67a18e"/>
+      <stop offset="0.95" stop-color="#37c78f"/>
+      <stop offset="1" stop-color="#22d88f"/>
+    </linearGradient>
+    <linearGradient id="linear-gradient-2" x1="32.58" y1="-665.27" x2="13.76" y2="-791.59" gradientTransform="matrix(1, 0, 0, -1, 0, -648.86)" gradientUnits="userSpaceOnUse">
+      <stop offset="0.09" stop-color="#22d88f"/>
+      <stop offset="0.9" stop-color="#029de0"/>
+    </linearGradient>
+    <linearGradient id="linear-gradient-3" x1="116.68" y1="-660.66" x2="-12.09" y2="-796.66" xlink:href="#linear-gradient-2"/>
+    <linearGradient id="linear-gradient-4" x1="73.35" y1="-739.1" x2="122.29" y2="-746.06" xlink:href="#linear-gradient-2"/>
+  </defs>
+  <title>icon_CLion</title>
+  <g>
+    <polygon points="49.2 51.8 40.6 55.4 48.4 0 77.8 16.2 49.2 51.8" fill="url(#linear-gradient)"/>
+    <polygon points="44.6 76.8 48.8 0 11.8 23.2 0 94 44.6 76.8" fill="url(#linear-gradient-2)"/>
+    <polygon points="125.4 38.4 109 4.8 77.8 16.2 55 41.4 0 94 41.6 124.4 93.6 77.2 125.4 38.4" fill="url(#linear-gradient-3)"/>
+    <polygon points="53.8 54.6 46.6 98.4 75.8 121 107.8 128 128 82.4 53.8 54.6" fill="url(#linear-gradient-4)"/>
+  </g>
+  <g>
+    <rect x="24" y="24" width="80" height="80"/>
+    <rect x="31.6" y="89" width="30" height="5" fill="#fff"/>
+    <path d="M31,51.2h0A16.83,16.83,0,0,1,48.2,34c6.2,0,10,2,13,5.2l-4.6,5.4c-2.6-2.4-5.2-3.8-8.4-3.8-5.6,0-9.6,4.6-9.6,10.4h0c0,5.6,4,10.4,9.6,10.4,3.8,0,6.2-1.6,8.8-3.8l4.6,4.6c-3.4,3.6-7.2,6-13.6,6A17,17,0,0,1,31,51.2" fill="#fff"/>
+    <path d="M66.6,34.4H74v27H88.4v6.2H66.6V34.4Z" fill="#fff"/>
+  </g>
+</svg>
diff --git a/src/chunked_encoder.rs b/src/chunked_encoder.rs
new file mode 100644
index 0000000..bd45ec9
--- /dev/null
+++ b/src/chunked_encoder.rs
@@ -0,0 +1,247 @@
+use crate::{
+    encode::{add_padding, encode_to_slice},
+    Config,
+};
+#[cfg(any(feature = "alloc", feature = "std", test))]
+use alloc::string::String;
+use core::cmp;
+#[cfg(any(feature = "alloc", feature = "std", test))]
+use core::str;
+
+/// The output mechanism for ChunkedEncoder's encoded bytes.
+pub trait Sink {
+    type Error;
+
+    /// Handle a chunk of encoded base64 data (as UTF-8 bytes)
+    fn write_encoded_bytes(&mut self, encoded: &[u8]) -> Result<(), Self::Error>;
+}
+
+const BUF_SIZE: usize = 1024;
+
+/// A base64 encoder that emits encoded bytes in chunks without heap allocation.
+pub struct ChunkedEncoder {
+    config: Config,
+    max_input_chunk_len: usize,
+}
+
+impl ChunkedEncoder {
+    pub fn new(config: Config) -> ChunkedEncoder {
+        ChunkedEncoder {
+            config,
+            max_input_chunk_len: max_input_length(BUF_SIZE, config),
+        }
+    }
+
+    pub fn encode<S: Sink>(&self, bytes: &[u8], sink: &mut S) -> Result<(), S::Error> {
+        let mut encode_buf: [u8; BUF_SIZE] = [0; BUF_SIZE];
+        let encode_table = self.config.char_set.encode_table();
+
+        let mut input_index = 0;
+
+        while input_index < bytes.len() {
+            // either the full input chunk size, or it's the last iteration
+            let input_chunk_len = cmp::min(self.max_input_chunk_len, bytes.len() - input_index);
+
+            let chunk = &bytes[input_index..(input_index + input_chunk_len)];
+
+            let mut b64_bytes_written = encode_to_slice(chunk, &mut encode_buf, encode_table);
+
+            input_index += input_chunk_len;
+            let more_input_left = input_index < bytes.len();
+
+            if self.config.pad && !more_input_left {
+                // no more input, add padding if needed. Buffer will have room because
+                // max_input_length leaves room for it.
+                b64_bytes_written += add_padding(bytes.len(), &mut encode_buf[b64_bytes_written..]);
+            }
+
+            sink.write_encoded_bytes(&encode_buf[0..b64_bytes_written])?;
+        }
+
+        Ok(())
+    }
+}
+
+/// Calculate the longest input that can be encoded for the given output buffer size.
+///
+/// If the config requires padding, two bytes of buffer space will be set aside so that the last
+/// chunk of input can be encoded safely.
+///
+/// The input length will always be a multiple of 3 so that no encoding state has to be carried over
+/// between chunks.
+fn max_input_length(encoded_buf_len: usize, config: Config) -> usize {
+    let effective_buf_len = if config.pad {
+        // make room for padding
+        encoded_buf_len
+            .checked_sub(2)
+            .expect("Don't use a tiny buffer")
+    } else {
+        encoded_buf_len
+    };
+
+    // No padding, so just normal base64 expansion.
+    (effective_buf_len / 4) * 3
+}
+
+// A really simple sink that just appends to a string
+#[cfg(any(feature = "alloc", feature = "std", test))]
+pub(crate) struct StringSink<'a> {
+    string: &'a mut String,
+}
+
+#[cfg(any(feature = "alloc", feature = "std", test))]
+impl<'a> StringSink<'a> {
+    pub(crate) fn new(s: &mut String) -> StringSink {
+        StringSink { string: s }
+    }
+}
+
+#[cfg(any(feature = "alloc", feature = "std", test))]
+impl<'a> Sink for StringSink<'a> {
+    type Error = ();
+
+    fn write_encoded_bytes(&mut self, s: &[u8]) -> Result<(), Self::Error> {
+        self.string.push_str(str::from_utf8(s).unwrap());
+
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+pub mod tests {
+    use super::*;
+    use crate::{encode_config_buf, tests::random_config, CharacterSet, STANDARD};
+
+    use rand::{
+        distributions::{Distribution, Uniform},
+        FromEntropy, Rng,
+    };
+
+    #[test]
+    fn chunked_encode_empty() {
+        assert_eq!("", chunked_encode_str(&[], STANDARD));
+    }
+
+    #[test]
+    fn chunked_encode_intermediate_fast_loop() {
+        // > 8 bytes input, will enter the pretty fast loop
+        assert_eq!(
+            "Zm9vYmFyYmF6cXV4",
+            chunked_encode_str(b"foobarbazqux", STANDARD)
+        );
+    }
+
+    #[test]
+    fn chunked_encode_fast_loop() {
+        // > 32 bytes input, will enter the uber fast loop
+        assert_eq!(
+            "Zm9vYmFyYmF6cXV4cXV1eGNvcmdlZ3JhdWx0Z2FycGx5eg==",
+            chunked_encode_str(b"foobarbazquxquuxcorgegraultgarplyz", STANDARD)
+        );
+    }
+
+    #[test]
+    fn chunked_encode_slow_loop_only() {
+        // < 8 bytes input, slow loop only
+        assert_eq!("Zm9vYmFy", chunked_encode_str(b"foobar", STANDARD));
+    }
+
+    #[test]
+    fn chunked_encode_matches_normal_encode_random_string_sink() {
+        let helper = StringSinkTestHelper;
+        chunked_encode_matches_normal_encode_random(&helper);
+    }
+
+    #[test]
+    fn max_input_length_no_pad() {
+        let config = config_with_pad(false);
+        assert_eq!(768, max_input_length(1024, config));
+    }
+
+    #[test]
+    fn max_input_length_with_pad_decrements_one_triple() {
+        let config = config_with_pad(true);
+        assert_eq!(765, max_input_length(1024, config));
+    }
+
+    #[test]
+    fn max_input_length_with_pad_one_byte_short() {
+        let config = config_with_pad(true);
+        assert_eq!(765, max_input_length(1025, config));
+    }
+
+    #[test]
+    fn max_input_length_with_pad_fits_exactly() {
+        let config = config_with_pad(true);
+        assert_eq!(768, max_input_length(1026, config));
+    }
+
+    #[test]
+    fn max_input_length_cant_use_extra_single_encoded_byte() {
+        let config = Config::new(crate::CharacterSet::Standard, false);
+        assert_eq!(300, max_input_length(401, config));
+    }
+
+    pub fn chunked_encode_matches_normal_encode_random<S: SinkTestHelper>(sink_test_helper: &S) {
+        let mut input_buf: Vec<u8> = Vec::new();
+        let mut output_buf = String::new();
+        let mut rng = rand::rngs::SmallRng::from_entropy();
+        let input_len_range = Uniform::new(1, 10_000);
+
+        for _ in 0..5_000 {
+            input_buf.clear();
+            output_buf.clear();
+
+            let buf_len = input_len_range.sample(&mut rng);
+            for _ in 0..buf_len {
+                input_buf.push(rng.gen());
+            }
+
+            let config = random_config(&mut rng);
+
+            let chunk_encoded_string = sink_test_helper.encode_to_string(config, &input_buf);
+            encode_config_buf(&input_buf, config, &mut output_buf);
+
+            assert_eq!(
+                output_buf, chunk_encoded_string,
+                "input len={}, config: pad={}",
+                buf_len, config.pad
+            );
+        }
+    }
+
+    fn chunked_encode_str(bytes: &[u8], config: Config) -> String {
+        let mut s = String::new();
+        {
+            let mut sink = StringSink::new(&mut s);
+            let encoder = ChunkedEncoder::new(config);
+            encoder.encode(bytes, &mut sink).unwrap();
+        }
+
+        return s;
+    }
+
+    fn config_with_pad(pad: bool) -> Config {
+        Config::new(CharacterSet::Standard, pad)
+    }
+
+    // An abstraction around sinks so that we can have tests that easily to any sink implementation
+    pub trait SinkTestHelper {
+        fn encode_to_string(&self, config: Config, bytes: &[u8]) -> String;
+    }
+
+    struct StringSinkTestHelper;
+
+    impl SinkTestHelper for StringSinkTestHelper {
+        fn encode_to_string(&self, config: Config, bytes: &[u8]) -> String {
+            let encoder = ChunkedEncoder::new(config);
+            let mut s = String::new();
+            {
+                let mut sink = StringSink::new(&mut s);
+                encoder.encode(bytes, &mut sink).unwrap();
+            }
+
+            s
+        }
+    }
+}
diff --git a/src/decode.rs b/src/decode.rs
new file mode 100644
index 0000000..4cc937d
--- /dev/null
+++ b/src/decode.rs
@@ -0,0 +1,873 @@
+use crate::{tables, Config, PAD_BYTE};
+
+#[cfg(any(feature = "alloc", feature = "std", test))]
+use crate::STANDARD;
+#[cfg(any(feature = "alloc", feature = "std", test))]
+use alloc::vec::Vec;
+use core::fmt;
+#[cfg(any(feature = "std", test))]
+use std::error;
+
+// decode logic operates on chunks of 8 input bytes without padding
+const INPUT_CHUNK_LEN: usize = 8;
+const DECODED_CHUNK_LEN: usize = 6;
+// we read a u64 and write a u64, but a u64 of input only yields 6 bytes of output, so the last
+// 2 bytes of any output u64 should not be counted as written to (but must be available in a
+// slice).
+const DECODED_CHUNK_SUFFIX: usize = 2;
+
+// how many u64's of input to handle at a time
+const CHUNKS_PER_FAST_LOOP_BLOCK: usize = 4;
+const INPUT_BLOCK_LEN: usize = CHUNKS_PER_FAST_LOOP_BLOCK * INPUT_CHUNK_LEN;
+// includes the trailing 2 bytes for the final u64 write
+const DECODED_BLOCK_LEN: usize =
+    CHUNKS_PER_FAST_LOOP_BLOCK * DECODED_CHUNK_LEN + DECODED_CHUNK_SUFFIX;
+
+/// Errors that can occur while decoding.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum DecodeError {
+    /// An invalid byte was found in the input. The offset and offending byte are provided.
+    InvalidByte(usize, u8),
+    /// The length of the input is invalid.
+    /// A typical cause of this is stray trailing whitespace or other separator bytes.
+    /// In the case where excess trailing bytes have produced an invalid length *and* the last byte
+    /// is also an invalid base64 symbol (as would be the case for whitespace, etc), `InvalidByte`
+    /// will be emitted instead of `InvalidLength` to make the issue easier to debug.
+    InvalidLength,
+    /// The last non-padding input symbol's encoded 6 bits have nonzero bits that will be discarded.
+    /// This is indicative of corrupted or truncated Base64.
+    /// Unlike InvalidByte, which reports symbols that aren't in the alphabet, this error is for
+    /// symbols that are in the alphabet but represent nonsensical encodings.
+    InvalidLastSymbol(usize, u8),
+}
+
+impl fmt::Display for DecodeError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match *self {
+            DecodeError::InvalidByte(index, byte) => {
+                write!(f, "Invalid byte {}, offset {}.", byte, index)
+            }
+            DecodeError::InvalidLength => write!(f, "Encoded text cannot have a 6-bit remainder."),
+            DecodeError::InvalidLastSymbol(index, byte) => {
+                write!(f, "Invalid last symbol {}, offset {}.", byte, index)
+            }
+        }
+    }
+}
+
+#[cfg(any(feature = "std", test))]
+impl error::Error for DecodeError {
+    fn description(&self) -> &str {
+        match *self {
+            DecodeError::InvalidByte(_, _) => "invalid byte",
+            DecodeError::InvalidLength => "invalid length",
+            DecodeError::InvalidLastSymbol(_, _) => "invalid last symbol",
+        }
+    }
+
+    fn cause(&self) -> Option<&dyn error::Error> {
+        None
+    }
+}
+
+///Decode from string reference as octets.
+///Returns a Result containing a Vec<u8>.
+///Convenience `decode_config(input, base64::STANDARD);`.
+///
+///# Example
+///
+///```rust
+///extern crate base64;
+///
+///fn main() {
+///    let bytes = base64::decode("aGVsbG8gd29ybGQ=").unwrap();
+///    println!("{:?}", bytes);
+///}
+///```
+#[cfg(any(feature = "alloc", feature = "std", test))]
+pub fn decode<T: AsRef<[u8]>>(input: T) -> Result<Vec<u8>, DecodeError> {
+    decode_config(input, STANDARD)
+}
+
+///Decode from string reference as octets.
+///Returns a Result containing a Vec<u8>.
+///
+///# Example
+///
+///```rust
+///extern crate base64;
+///
+///fn main() {
+///    let bytes = base64::decode_config("aGVsbG8gd29ybGR+Cg==", base64::STANDARD).unwrap();
+///    println!("{:?}", bytes);
+///
+///    let bytes_url = base64::decode_config("aGVsbG8gaW50ZXJuZXR-Cg==", base64::URL_SAFE).unwrap();
+///    println!("{:?}", bytes_url);
+///}
+///```
+#[cfg(any(feature = "alloc", feature = "std", test))]
+pub fn decode_config<T: AsRef<[u8]>>(input: T, config: Config) -> Result<Vec<u8>, DecodeError> {
+    let mut buffer = Vec::<u8>::with_capacity(input.as_ref().len() * 4 / 3);
+
+    decode_config_buf(input, config, &mut buffer).map(|_| buffer)
+}
+
+///Decode from string reference as octets.
+///Writes into the supplied buffer to avoid allocation.
+///Returns a Result containing an empty tuple, aka ().
+///
+///# Example
+///
+///```rust
+///extern crate base64;
+///
+///fn main() {
+///    let mut buffer = Vec::<u8>::new();
+///    base64::decode_config_buf("aGVsbG8gd29ybGR+Cg==", base64::STANDARD, &mut buffer).unwrap();
+///    println!("{:?}", buffer);
+///
+///    buffer.clear();
+///
+///    base64::decode_config_buf("aGVsbG8gaW50ZXJuZXR-Cg==", base64::URL_SAFE, &mut buffer)
+///        .unwrap();
+///    println!("{:?}", buffer);
+///}
+///```
+#[cfg(any(feature = "alloc", feature = "std", test))]
+pub fn decode_config_buf<T: AsRef<[u8]>>(
+    input: T,
+    config: Config,
+    buffer: &mut Vec<u8>,
+) -> Result<(), DecodeError> {
+    let input_bytes = input.as_ref();
+
+    let starting_output_len = buffer.len();
+
+    let num_chunks = num_chunks(input_bytes);
+    let decoded_len_estimate = num_chunks
+        .checked_mul(DECODED_CHUNK_LEN)
+        .and_then(|p| p.checked_add(starting_output_len))
+        .expect("Overflow when calculating output buffer length");
+    buffer.resize(decoded_len_estimate, 0);
+
+    let bytes_written;
+    {
+        let buffer_slice = &mut buffer.as_mut_slice()[starting_output_len..];
+        bytes_written = decode_helper(input_bytes, num_chunks, config, buffer_slice)?;
+    }
+
+    buffer.truncate(starting_output_len + bytes_written);
+
+    Ok(())
+}
+
+/// Decode the input into the provided output slice.
+///
+/// This will not write any bytes past exactly what is decoded (no stray garbage bytes at the end).
+///
+/// If you don't know ahead of time what the decoded length should be, size your buffer with a
+/// conservative estimate for the decoded length of an input: 3 bytes of output for every 4 bytes of
+/// input, rounded up, or in other words `(input_len + 3) / 4 * 3`.
+///
+/// If the slice is not large enough, this will panic.
+pub fn decode_config_slice<T: AsRef<[u8]>>(
+    input: T,
+    config: Config,
+    output: &mut [u8],
+) -> Result<usize, DecodeError> {
+    let input_bytes = input.as_ref();
+
+    decode_helper(input_bytes, num_chunks(input_bytes), config, output)
+}
+
+/// Return the number of input chunks (including a possibly partial final chunk) in the input
+fn num_chunks(input: &[u8]) -> usize {
+    input
+        .len()
+        .checked_add(INPUT_CHUNK_LEN - 1)
+        .expect("Overflow when calculating number of chunks in input")
+        / INPUT_CHUNK_LEN
+}
+
+/// Helper to avoid duplicating num_chunks calculation, which is costly on short inputs.
+/// Returns the number of bytes written, or an error.
+// We're on the fragile edge of compiler heuristics here. If this is not inlined, slow. If this is
+// inlined(always), a different slow. plain ol' inline makes the benchmarks happiest at the moment,
+// but this is fragile and the best setting changes with only minor code modifications.
+#[inline]
+fn decode_helper(
+    input: &[u8],
+    num_chunks: usize,
+    config: Config,
+    output: &mut [u8],
+) -> Result<usize, DecodeError> {
+    let char_set = config.char_set;
+    let decode_table = char_set.decode_table();
+
+    let remainder_len = input.len() % INPUT_CHUNK_LEN;
+
+    // Because the fast decode loop writes in groups of 8 bytes (unrolled to
+    // CHUNKS_PER_FAST_LOOP_BLOCK times 8 bytes, where possible) and outputs 8 bytes at a time (of
+    // which only 6 are valid data), we need to be sure that we stop using the fast decode loop
+    // soon enough that there will always be 2 more bytes of valid data written after that loop.
+    let trailing_bytes_to_skip = match remainder_len {
+        // if input is a multiple of the chunk size, ignore the last chunk as it may have padding,
+        // and the fast decode logic cannot handle padding
+        0 => INPUT_CHUNK_LEN,
+        // 1 and 5 trailing bytes are illegal: can't decode 6 bits of input into a byte
+        1 | 5 => {
+            // trailing whitespace is so common that it's worth it to check the last byte to
+            // possibly return a better error message
+            if let Some(b) = input.last() {
+                if *b != PAD_BYTE && decode_table[*b as usize] == tables::INVALID_VALUE {
+                    return Err(DecodeError::InvalidByte(input.len() - 1, *b));
+                }
+            }
+
+            return Err(DecodeError::InvalidLength);
+        }
+        // This will decode to one output byte, which isn't enough to overwrite the 2 extra bytes
+        // written by the fast decode loop. So, we have to ignore both these 2 bytes and the
+        // previous chunk.
+        2 => INPUT_CHUNK_LEN + 2,
+        // If this is 3 unpadded chars, then it would actually decode to 2 bytes. However, if this
+        // is an erroneous 2 chars + 1 pad char that would decode to 1 byte, then it should fail
+        // with an error, not panic from going past the bounds of the output slice, so we let it
+        // use stage 3 + 4.
+        3 => INPUT_CHUNK_LEN + 3,
+        // This can also decode to one output byte because it may be 2 input chars + 2 padding
+        // chars, which would decode to 1 byte.
+        4 => INPUT_CHUNK_LEN + 4,
+        // Everything else is a legal decode len (given that we don't require padding), and will
+        // decode to at least 2 bytes of output.
+        _ => remainder_len,
+    };
+
+    // rounded up to include partial chunks
+    let mut remaining_chunks = num_chunks;
+
+    let mut input_index = 0;
+    let mut output_index = 0;
+
+    {
+        let length_of_fast_decode_chunks = input.len().saturating_sub(trailing_bytes_to_skip);
+
+        // Fast loop, stage 1
+        // manual unroll to CHUNKS_PER_FAST_LOOP_BLOCK of u64s to amortize slice bounds checks
+        if let Some(max_start_index) = length_of_fast_decode_chunks.checked_sub(INPUT_BLOCK_LEN) {
+            while input_index <= max_start_index {
+                let input_slice = &input[input_index..(input_index + INPUT_BLOCK_LEN)];
+                let output_slice = &mut output[output_index..(output_index + DECODED_BLOCK_LEN)];
+
+                decode_chunk(
+                    &input_slice[0..],
+                    input_index,
+                    decode_table,
+                    &mut output_slice[0..],
+                )?;
+                decode_chunk(
+                    &input_slice[8..],
+                    input_index + 8,
+                    decode_table,
+                    &mut output_slice[6..],
+                )?;
+                decode_chunk(
+                    &input_slice[16..],
+                    input_index + 16,
+                    decode_table,
+                    &mut output_slice[12..],
+                )?;
+                decode_chunk(
+                    &input_slice[24..],
+                    input_index + 24,
+                    decode_table,
+                    &mut output_slice[18..],
+                )?;
+
+                input_index += INPUT_BLOCK_LEN;
+                output_index += DECODED_BLOCK_LEN - DECODED_CHUNK_SUFFIX;
+                remaining_chunks -= CHUNKS_PER_FAST_LOOP_BLOCK;
+            }
+        }
+
+        // Fast loop, stage 2 (aka still pretty fast loop)
+        // 8 bytes at a time for whatever we didn't do in stage 1.
+        if let Some(max_start_index) = length_of_fast_decode_chunks.checked_sub(INPUT_CHUNK_LEN) {
+            while input_index < max_start_index {
+                decode_chunk(
+                    &input[input_index..(input_index + INPUT_CHUNK_LEN)],
+                    input_index,
+                    decode_table,
+                    &mut output
+                        [output_index..(output_index + DECODED_CHUNK_LEN + DECODED_CHUNK_SUFFIX)],
+                )?;
+
+                output_index += DECODED_CHUNK_LEN;
+                input_index += INPUT_CHUNK_LEN;
+                remaining_chunks -= 1;
+            }
+        }
+    }
+
+    // Stage 3
+    // If input length was such that a chunk had to be deferred until after the fast loop
+    // because decoding it would have produced 2 trailing bytes that wouldn't then be
+    // overwritten, we decode that chunk here. This way is slower but doesn't write the 2
+    // trailing bytes.
+    // However, we still need to avoid the last chunk (partial or complete) because it could
+    // have padding, so we always do 1 fewer to avoid the last chunk.
+    for _ in 1..remaining_chunks {
+        decode_chunk_precise(
+            &input[input_index..],
+            input_index,
+            decode_table,
+            &mut output[output_index..(output_index + DECODED_CHUNK_LEN)],
+        )?;
+
+        input_index += INPUT_CHUNK_LEN;
+        output_index += DECODED_CHUNK_LEN;
+    }
+
+    // always have one more (possibly partial) block of 8 input
+    debug_assert!(input.len() - input_index > 1 || input.is_empty());
+    debug_assert!(input.len() - input_index <= 8);
+
+    // Stage 4
+    // Finally, decode any leftovers that aren't a complete input block of 8 bytes.
+    // Use a u64 as a stack-resident 8 byte buffer.
+    let mut leftover_bits: u64 = 0;
+    let mut morsels_in_leftover = 0;
+    let mut padding_bytes = 0;
+    let mut first_padding_index: usize = 0;
+    let mut last_symbol = 0_u8;
+    let start_of_leftovers = input_index;
+    for (i, b) in input[start_of_leftovers..].iter().enumerate() {
+        // '=' padding
+        if *b == PAD_BYTE {
+            // There can be bad padding in a few ways:
+            // 1 - Padding with non-padding characters after it
+            // 2 - Padding after zero or one non-padding characters before it
+            //     in the current quad.
+            // 3 - More than two characters of padding. If 3 or 4 padding chars
+            //     are in the same quad, that implies it will be caught by #2.
+            //     If it spreads from one quad to another, it will be caught by
+            //     #2 in the second quad.
+
+            if i % 4 < 2 {
+                // Check for case #2.
+                let bad_padding_index = start_of_leftovers
+                    + if padding_bytes > 0 {
+                        // If we've already seen padding, report the first padding index.
+                        // This is to be consistent with the faster logic above: it will report an
+                        // error on the first padding character (since it doesn't expect to see
+                        // anything but actual encoded data).
+                        first_padding_index
+                    } else {
+                        // haven't seen padding before, just use where we are now
+                        i
+                    };
+                return Err(DecodeError::InvalidByte(bad_padding_index, *b));
+            }
+
+            if padding_bytes == 0 {
+                first_padding_index = i;
+            }
+
+            padding_bytes += 1;
+            continue;
+        }
+
+        // Check for case #1.
+        // To make '=' handling consistent with the main loop, don't allow
+        // non-suffix '=' in trailing chunk either. Report error as first
+        // erroneous padding.
+        if padding_bytes > 0 {
+            return Err(DecodeError::InvalidByte(
+                start_of_leftovers + first_padding_index,
+                PAD_BYTE,
+            ));
+        }
+        last_symbol = *b;
+
+        // can use up to 8 * 6 = 48 bits of the u64, if last chunk has no padding.
+        // To minimize shifts, pack the leftovers from left to right.
+        let shift = 64 - (morsels_in_leftover + 1) * 6;
+        // tables are all 256 elements, lookup with a u8 index always succeeds
+        let morsel = decode_table[*b as usize];
+        if morsel == tables::INVALID_VALUE {
+            return Err(DecodeError::InvalidByte(start_of_leftovers + i, *b));
+        }
+
+        leftover_bits |= (morsel as u64) << shift;
+        morsels_in_leftover += 1;
+    }
+
+    let leftover_bits_ready_to_append = match morsels_in_leftover {
+        0 => 0,
+        2 => 8,
+        3 => 16,
+        4 => 24,
+        6 => 32,
+        7 => 40,
+        8 => 48,
+        _ => unreachable!(
+            "Impossible: must only have 0 to 8 input bytes in last chunk, with no invalid lengths"
+        ),
+    };
+
+    // if there are bits set outside the bits we care about, last symbol encodes trailing bits that
+    // will not be included in the output
+    let mask = !0 >> leftover_bits_ready_to_append;
+    if !config.decode_allow_trailing_bits && (leftover_bits & mask) != 0 {
+        // last morsel is at `morsels_in_leftover` - 1
+        return Err(DecodeError::InvalidLastSymbol(
+            start_of_leftovers + morsels_in_leftover - 1,
+            last_symbol,
+        ));
+    }
+
+    let mut leftover_bits_appended_to_buf = 0;
+    while leftover_bits_appended_to_buf < leftover_bits_ready_to_append {
+        // `as` simply truncates the higher bits, which is what we want here
+        let selected_bits = (leftover_bits >> (56 - leftover_bits_appended_to_buf)) as u8;
+        output[output_index] = selected_bits;
+        output_index += 1;
+
+        leftover_bits_appended_to_buf += 8;
+    }
+
+    Ok(output_index)
+}
+
+#[inline]
+fn write_u64(output: &mut [u8], value: u64) {
+    output[..8].copy_from_slice(&value.to_be_bytes());
+}
+
+/// Decode 8 bytes of input into 6 bytes of output. 8 bytes of output will be written, but only the
+/// first 6 of those contain meaningful data.
+///
+/// `input` is the bytes to decode, of which the first 8 bytes will be processed.
+/// `index_at_start_of_input` is the offset in the overall input (used for reporting errors
+/// accurately)
+/// `decode_table` is the lookup table for the particular base64 alphabet.
+/// `output` will have its first 8 bytes overwritten, of which only the first 6 are valid decoded
+/// data.
+// yes, really inline (worth 30-50% speedup)
+#[inline(always)]
+fn decode_chunk(
+    input: &[u8],
+    index_at_start_of_input: usize,
+    decode_table: &[u8; 256],
+    output: &mut [u8],
+) -> Result<(), DecodeError> {
+    let mut accum: u64;
+
+    let morsel = decode_table[input[0] as usize];
+    if morsel == tables::INVALID_VALUE {
+        return Err(DecodeError::InvalidByte(index_at_start_of_input, input[0]));
+    }
+    accum = (morsel as u64) << 58;
+
+    let morsel = decode_table[input[1] as usize];
+    if morsel == tables::INVALID_VALUE {
+        return Err(DecodeError::InvalidByte(
+            index_at_start_of_input + 1,
+            input[1],
+        ));
+    }
+    accum |= (morsel as u64) << 52;
+
+    let morsel = decode_table[input[2] as usize];
+    if morsel == tables::INVALID_VALUE {
+        return Err(DecodeError::InvalidByte(
+            index_at_start_of_input + 2,
+            input[2],
+        ));
+    }
+    accum |= (morsel as u64) << 46;
+
+    let morsel = decode_table[input[3] as usize];
+    if morsel == tables::INVALID_VALUE {
+        return Err(DecodeError::InvalidByte(
+            index_at_start_of_input + 3,
+            input[3],
+        ));
+    }
+    accum |= (morsel as u64) << 40;
+
+    let morsel = decode_table[input[4] as usize];
+    if morsel == tables::INVALID_VALUE {
+        return Err(DecodeError::InvalidByte(
+            index_at_start_of_input + 4,
+            input[4],
+        ));
+    }
+    accum |= (morsel as u64) << 34;
+
+    let morsel = decode_table[input[5] as usize];
+    if morsel == tables::INVALID_VALUE {
+        return Err(DecodeError::InvalidByte(
+            index_at_start_of_input + 5,
+            input[5],
+        ));
+    }
+    accum |= (morsel as u64) << 28;
+
+    let morsel = decode_table[input[6] as usize];
+    if morsel == tables::INVALID_VALUE {
+        return Err(DecodeError::InvalidByte(
+            index_at_start_of_input + 6,
+            input[6],
+        ));
+    }
+    accum |= (morsel as u64) << 22;
+
+    let morsel = decode_table[input[7] as usize];
+    if morsel == tables::INVALID_VALUE {
+        return Err(DecodeError::InvalidByte(
+            index_at_start_of_input + 7,
+            input[7],
+        ));
+    }
+    accum |= (morsel as u64) << 16;
+
+    write_u64(output, accum);
+
+    Ok(())
+}
+
+/// Decode an 8-byte chunk, but only write the 6 bytes actually decoded instead of including 2
+/// trailing garbage bytes.
+#[inline]
+fn decode_chunk_precise(
+    input: &[u8],
+    index_at_start_of_input: usize,
+    decode_table: &[u8; 256],
+    output: &mut [u8],
+) -> Result<(), DecodeError> {
+    let mut tmp_buf = [0_u8; 8];
+
+    decode_chunk(
+        input,
+        index_at_start_of_input,
+        decode_table,
+        &mut tmp_buf[..],
+    )?;
+
+    output[0..6].copy_from_slice(&tmp_buf[0..6]);
+
+    Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{
+        encode::encode_config_buf,
+        encode::encode_config_slice,
+        tests::{assert_encode_sanity, random_config},
+    };
+
+    use rand::{
+        distributions::{Distribution, Uniform},
+        FromEntropy, Rng,
+    };
+
+    #[test]
+    fn decode_chunk_precise_writes_only_6_bytes() {
+        let input = b"Zm9vYmFy"; // "foobar"
+        let mut output = [0_u8, 1, 2, 3, 4, 5, 6, 7];
+        decode_chunk_precise(&input[..], 0, tables::STANDARD_DECODE, &mut output).unwrap();
+        assert_eq!(&vec![b'f', b'o', b'o', b'b', b'a', b'r', 6, 7], &output);
+    }
+
+    #[test]
+    fn decode_chunk_writes_8_bytes() {
+        let input = b"Zm9vYmFy"; // "foobar"
+        let mut output = [0_u8, 1, 2, 3, 4, 5, 6, 7];
+        decode_chunk(&input[..], 0, tables::STANDARD_DECODE, &mut output).unwrap();
+        assert_eq!(&vec![b'f', b'o', b'o', b'b', b'a', b'r', 0, 0], &output);
+    }
+
+    #[test]
+    fn decode_into_nonempty_vec_doesnt_clobber_existing_prefix() {
+        let mut orig_data = Vec::new();
+        let mut encoded_data = String::new();
+        let mut decoded_with_prefix = Vec::new();
+        let mut decoded_without_prefix = Vec::new();
+        let mut prefix = Vec::new();
+
+        let prefix_len_range = Uniform::new(0, 1000);
+        let input_len_range = Uniform::new(0, 1000);
+
+        let mut rng = rand::rngs::SmallRng::from_entropy();
+
+        for _ in 0..10_000 {
+            orig_data.clear();
+            encoded_data.clear();
+            decoded_with_prefix.clear();
+            decoded_without_prefix.clear();
+            prefix.clear();
+
+            let input_len = input_len_range.sample(&mut rng);
+
+            for _ in 0..input_len {
+                orig_data.push(rng.gen());
+            }
+
+            let config = random_config(&mut rng);
+            encode_config_buf(&orig_data, config, &mut encoded_data);
+            assert_encode_sanity(&encoded_data, config, input_len);
+
+            let prefix_len = prefix_len_range.sample(&mut rng);
+
+            // fill the buf with a prefix
+            for _ in 0..prefix_len {
+                prefix.push(rng.gen());
+            }
+
+            decoded_with_prefix.resize(prefix_len, 0);
+            decoded_with_prefix.copy_from_slice(&prefix);
+
+            // decode into the non-empty buf
+            decode_config_buf(&encoded_data, config, &mut decoded_with_prefix).unwrap();
+            // also decode into the empty buf
+            decode_config_buf(&encoded_data, config, &mut decoded_without_prefix).unwrap();
+
+            assert_eq!(
+                prefix_len + decoded_without_prefix.len(),
+                decoded_with_prefix.len()
+            );
+            assert_eq!(orig_data, decoded_without_prefix);
+
+            // append plain decode onto prefix
+            prefix.append(&mut decoded_without_prefix);
+
+            assert_eq!(prefix, decoded_with_prefix);
+        }
+    }
+
+    #[test]
+    fn decode_into_slice_doesnt_clobber_existing_prefix_or_suffix() {
+        let mut orig_data = Vec::new();
+        let mut encoded_data = String::new();
+        let mut decode_buf = Vec::new();
+        let mut decode_buf_copy: Vec<u8> = Vec::new();
+
+        let input_len_range = Uniform::new(0, 1000);
+
+        let mut rng = rand::rngs::SmallRng::from_entropy();
+
+        for _ in 0..10_000 {
+            orig_data.clear();
+            encoded_data.clear();
+            decode_buf.clear();
+            decode_buf_copy.clear();
+
+            let input_len = input_len_range.sample(&mut rng);
+
+            for _ in 0..input_len {
+                orig_data.push(rng.gen());
+            }
+
+            let config = random_config(&mut rng);
+            encode_config_buf(&orig_data, config, &mut encoded_data);
+            assert_encode_sanity(&encoded_data, config, input_len);
+
+            // fill the buffer with random garbage, long enough to have some room before and after
+            for _ in 0..5000 {
+                decode_buf.push(rng.gen());
+            }
+
+            // keep a copy for later comparison
+            decode_buf_copy.extend(decode_buf.iter());
+
+            let offset = 1000;
+
+            // decode into the non-empty buf
+            let decode_bytes_written =
+                decode_config_slice(&encoded_data, config, &mut decode_buf[offset..]).unwrap();
+
+            assert_eq!(orig_data.len(), decode_bytes_written);
+            assert_eq!(
+                orig_data,
+                &decode_buf[offset..(offset + decode_bytes_written)]
+            );
+            assert_eq!(&decode_buf_copy[0..offset], &decode_buf[0..offset]);
+            assert_eq!(
+                &decode_buf_copy[offset + decode_bytes_written..],
+                &decode_buf[offset + decode_bytes_written..]
+            );
+        }
+    }
+
+    #[test]
+    fn decode_into_slice_fits_in_precisely_sized_slice() {
+        let mut orig_data = Vec::new();
+        let mut encoded_data = String::new();
+        let mut decode_buf = Vec::new();
+
+        let input_len_range = Uniform::new(0, 1000);
+
+        let mut rng = rand::rngs::SmallRng::from_entropy();
+
+        for _ in 0..10_000 {
+            orig_data.clear();
+            encoded_data.clear();
+            decode_buf.clear();
+
+            let input_len = input_len_range.sample(&mut rng);
+
+            for _ in 0..input_len {
+                orig_data.push(rng.gen());
+            }
+
+            let config = random_config(&mut rng);
+            encode_config_buf(&orig_data, config, &mut encoded_data);
+            assert_encode_sanity(&encoded_data, config, input_len);
+
+            decode_buf.resize(input_len, 0);
+
+            // decode into the non-empty buf
+            let decode_bytes_written =
+                decode_config_slice(&encoded_data, config, &mut decode_buf[..]).unwrap();
+
+            assert_eq!(orig_data.len(), decode_bytes_written);
+            assert_eq!(orig_data, decode_buf);
+        }
+    }
+
+    #[test]
+    fn detect_invalid_last_symbol_two_bytes() {
+        let decode =
+            |input, forgiving| decode_config(input, STANDARD.decode_allow_trailing_bits(forgiving));
+
+        // example from https://github.com/marshallpierce/rust-base64/issues/75
+        assert!(decode("iYU=", false).is_ok());
+        // trailing 01
+        assert_eq!(
+            Err(DecodeError::InvalidLastSymbol(2, b'V')),
+            decode("iYV=", false)
+        );
+        assert_eq!(Ok(vec![137, 133]), decode("iYV=", true));
+        // trailing 10
+        assert_eq!(
+            Err(DecodeError::InvalidLastSymbol(2, b'W')),
+            decode("iYW=", false)
+        );
+        assert_eq!(Ok(vec![137, 133]), decode("iYV=", true));
+        // trailing 11
+        assert_eq!(
+            Err(DecodeError::InvalidLastSymbol(2, b'X')),
+            decode("iYX=", false)
+        );
+        assert_eq!(Ok(vec![137, 133]), decode("iYV=", true));
+
+        // also works when there are 2 quads in the last block
+        assert_eq!(
+            Err(DecodeError::InvalidLastSymbol(6, b'X')),
+            decode("AAAAiYX=", false)
+        );
+        assert_eq!(Ok(vec![0, 0, 0, 137, 133]), decode("AAAAiYX=", true));
+    }
+
+    #[test]
+    fn detect_invalid_last_symbol_one_byte() {
+        // 0xFF -> "/w==", so all letters > w, 0-9, and '+', '/' should get InvalidLastSymbol
+
+        assert!(decode("/w==").is_ok());
+        // trailing 01
+        assert_eq!(Err(DecodeError::InvalidLastSymbol(1, b'x')), decode("/x=="));
+        assert_eq!(Err(DecodeError::InvalidLastSymbol(1, b'z')), decode("/z=="));
+        assert_eq!(Err(DecodeError::InvalidLastSymbol(1, b'0')), decode("/0=="));
+        assert_eq!(Err(DecodeError::InvalidLastSymbol(1, b'9')), decode("/9=="));
+        assert_eq!(Err(DecodeError::InvalidLastSymbol(1, b'+')), decode("/+=="));
+        assert_eq!(Err(DecodeError::InvalidLastSymbol(1, b'/')), decode("//=="));
+
+        // also works when there are 2 quads in the last block
+        assert_eq!(
+            Err(DecodeError::InvalidLastSymbol(5, b'x')),
+            decode("AAAA/x==")
+        );
+    }
+
+    #[test]
+    fn detect_invalid_last_symbol_every_possible_three_symbols() {
+        let mut base64_to_bytes = ::std::collections::HashMap::new();
+
+        let mut bytes = [0_u8; 2];
+        for b1 in 0_u16..256 {
+            bytes[0] = b1 as u8;
+            for b2 in 0_u16..256 {
+                bytes[1] = b2 as u8;
+                let mut b64 = vec![0_u8; 4];
+                assert_eq!(4, encode_config_slice(&bytes, STANDARD, &mut b64[..]));
+                let mut v = ::std::vec::Vec::with_capacity(2);
+                v.extend_from_slice(&bytes[..]);
+
+                assert!(base64_to_bytes.insert(b64, v).is_none());
+            }
+        }
+
+        // every possible combination of symbols must either decode to 2 bytes or get InvalidLastSymbol
+
+        let mut symbols = [0_u8; 4];
+        for &s1 in STANDARD.char_set.encode_table().iter() {
+            symbols[0] = s1;
+            for &s2 in STANDARD.char_set.encode_table().iter() {
+                symbols[1] = s2;
+                for &s3 in STANDARD.char_set.encode_table().iter() {
+                    symbols[2] = s3;
+                    symbols[3] = PAD_BYTE;
+
+                    match base64_to_bytes.get(&symbols[..]) {
+                        Some(bytes) => {
+                            assert_eq!(Ok(bytes.to_vec()), decode_config(&symbols, STANDARD))
+                        }
+                        None => assert_eq!(
+                            Err(DecodeError::InvalidLastSymbol(2, s3)),
+                            decode_config(&symbols[..], STANDARD)
+                        ),
+                    }
+                }
+            }
+        }
+    }
+
+    #[test]
+    fn detect_invalid_last_symbol_every_possible_two_symbols() {
+        let mut base64_to_bytes = ::std::collections::HashMap::new();
+
+        for b in 0_u16..256 {
+            let mut b64 = vec![0_u8; 4];
+            assert_eq!(4, encode_config_slice(&[b as u8], STANDARD, &mut b64[..]));
+            let mut v = ::std::vec::Vec::with_capacity(1);
+            v.push(b as u8);
+
+            assert!(base64_to_bytes.insert(b64, v).is_none());
+        }
+
+        // every possible combination of symbols must either decode to 1 byte or get InvalidLastSymbol
+
+        let mut symbols = [0_u8; 4];
+        for &s1 in STANDARD.char_set.encode_table().iter() {
+            symbols[0] = s1;
+            for &s2 in STANDARD.char_set.encode_table().iter() {
+                symbols[1] = s2;
+                symbols[2] = PAD_BYTE;
+                symbols[3] = PAD_BYTE;
+
+                match base64_to_bytes.get(&symbols[..]) {
+                    Some(bytes) => {
+                        assert_eq!(Ok(bytes.to_vec()), decode_config(&symbols, STANDARD))
+                    }
+                    None => assert_eq!(
+                        Err(DecodeError::InvalidLastSymbol(1, s2)),
+                        decode_config(&symbols[..], STANDARD)
+                    ),
+                }
+            }
+        }
+    }
+}
diff --git a/src/display.rs b/src/display.rs
new file mode 100644
index 0000000..cc70aac
--- /dev/null
+++ b/src/display.rs
@@ -0,0 +1,88 @@
+//! Enables base64'd output anywhere you might use a `Display` implementation, like a format string.
+//!
+//! ```
+//! use base64::display::Base64Display;
+//!
+//! let data = vec![0x0, 0x1, 0x2, 0x3];
+//! let wrapper = Base64Display::with_config(&data, base64::STANDARD);
+//!
+//! assert_eq!("base64: AAECAw==", format!("base64: {}", wrapper));
+//! ```
+
+use super::chunked_encoder::ChunkedEncoder;
+use super::Config;
+use core::fmt::{Display, Formatter};
+use core::{fmt, str};
+
+/// A convenience wrapper for base64'ing bytes into a format string without heap allocation.
+pub struct Base64Display<'a> {
+    bytes: &'a [u8],
+    chunked_encoder: ChunkedEncoder,
+}
+
+impl<'a> Base64Display<'a> {
+    /// Create a `Base64Display` with the provided config.
+    pub fn with_config(bytes: &[u8], config: Config) -> Base64Display {
+        Base64Display {
+            bytes,
+            chunked_encoder: ChunkedEncoder::new(config),
+        }
+    }
+}
+
+impl<'a> Display for Base64Display<'a> {
+    fn fmt(&self, formatter: &mut Formatter) -> Result<(), fmt::Error> {
+        let mut sink = FormatterSink { f: formatter };
+        self.chunked_encoder.encode(self.bytes, &mut sink)
+    }
+}
+
+struct FormatterSink<'a, 'b: 'a> {
+    f: &'a mut Formatter<'b>,
+}
+
+impl<'a, 'b: 'a> super::chunked_encoder::Sink for FormatterSink<'a, 'b> {
+    type Error = fmt::Error;
+
+    fn write_encoded_bytes(&mut self, encoded: &[u8]) -> Result<(), Self::Error> {
+        // Avoid unsafe. If max performance is needed, write your own display wrapper that uses
+        // unsafe here to gain about 10-15%.
+        self.f
+            .write_str(str::from_utf8(encoded).expect("base64 data was not utf8"))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::super::chunked_encoder::tests::{
+        chunked_encode_matches_normal_encode_random, SinkTestHelper,
+    };
+    use super::super::*;
+    use super::*;
+
+    #[test]
+    fn basic_display() {
+        assert_eq!(
+            "~$Zm9vYmFy#*",
+            format!("~${}#*", Base64Display::with_config(b"foobar", STANDARD))
+        );
+        assert_eq!(
+            "~$Zm9vYmFyZg==#*",
+            format!("~${}#*", Base64Display::with_config(b"foobarf", STANDARD))
+        );
+    }
+
+    #[test]
+    fn display_encode_matches_normal_encode() {
+        let helper = DisplaySinkTestHelper;
+        chunked_encode_matches_normal_encode_random(&helper);
+    }
+
+    struct DisplaySinkTestHelper;
+
+    impl SinkTestHelper for DisplaySinkTestHelper {
+        fn encode_to_string(&self, config: Config, bytes: &[u8]) -> String {
+            format!("{}", Base64Display::with_config(bytes, config))
+        }
+    }
+}
diff --git a/src/encode.rs b/src/encode.rs
new file mode 100644
index 0000000..b32bbff
--- /dev/null
+++ b/src/encode.rs
@@ -0,0 +1,675 @@
+use crate::{Config, PAD_BYTE};
+#[cfg(any(feature = "alloc", feature = "std", test))]
+use crate::{chunked_encoder, STANDARD};
+#[cfg(any(feature = "alloc", feature = "std", test))]
+use alloc::{string::String, vec};
+use core::convert::TryInto;
+
+///Encode arbitrary octets as base64.
+///Returns a String.
+///Convenience for `encode_config(input, base64::STANDARD);`.
+///
+///# Example
+///
+///```rust
+///extern crate base64;
+///
+///fn main() {
+///    let b64 = base64::encode(b"hello world");
+///    println!("{}", b64);
+///}
+///```
+#[cfg(any(feature = "alloc", feature = "std", test))]
+pub fn encode<T: AsRef<[u8]>>(input: T) -> String {
+    encode_config(input, STANDARD)
+}
+
+///Encode arbitrary octets as base64.
+///Returns a String.
+///
+///# Example
+///
+///```rust
+///extern crate base64;
+///
+///fn main() {
+///    let b64 = base64::encode_config(b"hello world~", base64::STANDARD);
+///    println!("{}", b64);
+///
+///    let b64_url = base64::encode_config(b"hello internet~", base64::URL_SAFE);
+///    println!("{}", b64_url);
+///}
+///```
+#[cfg(any(feature = "alloc", feature = "std", test))]
+pub fn encode_config<T: AsRef<[u8]>>(input: T, config: Config) -> String {
+    let mut buf = match encoded_size(input.as_ref().len(), config) {
+        Some(n) => vec![0; n],
+        None => panic!("integer overflow when calculating buffer size"),
+    };
+
+    encode_with_padding(input.as_ref(), config, buf.len(), &mut buf[..]);
+
+    String::from_utf8(buf).expect("Invalid UTF8")
+}
+
+///Encode arbitrary octets as base64.
+///Writes into the supplied output buffer, which will grow the buffer if needed.
+///
+///# Example
+///
+///```rust
+///extern crate base64;
+///
+///fn main() {
+///    let mut buf = String::new();
+///    base64::encode_config_buf(b"hello world~", base64::STANDARD, &mut buf);
+///    println!("{}", buf);
+///
+///    buf.clear();
+///    base64::encode_config_buf(b"hello internet~", base64::URL_SAFE, &mut buf);
+///    println!("{}", buf);
+///}
+///```
+#[cfg(any(feature = "alloc", feature = "std", test))]
+pub fn encode_config_buf<T: AsRef<[u8]>>(input: T, config: Config, buf: &mut String) {
+    let input_bytes = input.as_ref();
+
+    {
+        let mut sink = chunked_encoder::StringSink::new(buf);
+        let encoder = chunked_encoder::ChunkedEncoder::new(config);
+
+        encoder
+            .encode(input_bytes, &mut sink)
+            .expect("Writing to a String shouldn't fail")
+    }
+}
+
+/// Encode arbitrary octets as base64.
+/// Writes into the supplied output buffer.
+///
+/// This is useful if you wish to avoid allocation entirely (e.g. encoding into a stack-resident
+/// or statically-allocated buffer).
+///
+/// # Panics
+///
+/// If `output` is too small to hold the encoded version of `input`, a panic will result.
+///
+/// # Example
+///
+/// ```rust
+/// extern crate base64;
+///
+/// fn main() {
+///     let s = b"hello internet!";
+///     let mut buf = Vec::new();
+///     // make sure we'll have a slice big enough for base64 + padding
+///     buf.resize(s.len() * 4 / 3 + 4, 0);
+///
+///     let bytes_written = base64::encode_config_slice(s,
+///                             base64::STANDARD, &mut buf);
+///
+///     // shorten our vec down to just what was written
+///     buf.resize(bytes_written, 0);
+///
+///     assert_eq!(s, base64::decode(&buf).unwrap().as_slice());
+/// }
+/// ```
+pub fn encode_config_slice<T: AsRef<[u8]>>(input: T, config: Config, output: &mut [u8]) -> usize {
+    let input_bytes = input.as_ref();
+
+    let encoded_size = encoded_size(input_bytes.len(), config)
+        .expect("usize overflow when calculating buffer size");
+
+    let mut b64_output = &mut output[0..encoded_size];
+
+    encode_with_padding(&input_bytes, config, encoded_size, &mut b64_output);
+
+    encoded_size
+}
+
+/// B64-encode and pad (if configured).
+///
+/// This helper exists to avoid recalculating encoded_size, which is relatively expensive on short
+/// inputs.
+///
+/// `encoded_size` is the encoded size calculated for `input`.
+///
+/// `output` must be of size `encoded_size`.
+///
+/// All bytes in `output` will be written to since it is exactly the size of the output.
+fn encode_with_padding(input: &[u8], config: Config, encoded_size: usize, output: &mut [u8]) {
+    debug_assert_eq!(encoded_size, output.len());
+
+    let b64_bytes_written = encode_to_slice(input, output, config.char_set.encode_table());
+
+    let padding_bytes = if config.pad {
+        add_padding(input.len(), &mut output[b64_bytes_written..])
+    } else {
+        0
+    };
+
+    let encoded_bytes = b64_bytes_written
+        .checked_add(padding_bytes)
+        .expect("usize overflow when calculating b64 length");
+
+    debug_assert_eq!(encoded_size, encoded_bytes);
+}
+
+#[inline]
+fn read_u64(s: &[u8]) -> u64 {
+    u64::from_be_bytes(s[..8].try_into().unwrap())
+}
+
+/// Encode input bytes to utf8 base64 bytes. Does not pad.
+/// `output` must be long enough to hold the encoded `input` without padding.
+/// Returns the number of bytes written.
+#[inline]
+pub fn encode_to_slice(input: &[u8], output: &mut [u8], encode_table: &[u8; 64]) -> usize {
+    let mut input_index: usize = 0;
+
+    const BLOCKS_PER_FAST_LOOP: usize = 4;
+    const LOW_SIX_BITS: u64 = 0x3F;
+
+    // we read 8 bytes at a time (u64) but only actually consume 6 of those bytes. Thus, we need
+    // 2 trailing bytes to be available to read..
+    let last_fast_index = input.len().saturating_sub(BLOCKS_PER_FAST_LOOP * 6 + 2);
+    let mut output_index = 0;
+
+    if last_fast_index > 0 {
+        while input_index <= last_fast_index {
+            // Major performance wins from letting the optimizer do the bounds check once, mostly
+            // on the output side
+            let input_chunk = &input[input_index..(input_index + (BLOCKS_PER_FAST_LOOP * 6 + 2))];
+            let output_chunk = &mut output[output_index..(output_index + BLOCKS_PER_FAST_LOOP * 8)];
+
+            // Hand-unrolling for 32 vs 16 or 8 bytes produces yields performance about equivalent
+            // to unsafe pointer code on a Xeon E5-1650v3. 64 byte unrolling was slightly better for
+            // large inputs but significantly worse for 50-byte input, unsurprisingly. I suspect
+            // that it's a not uncommon use case to encode smallish chunks of data (e.g. a 64-byte
+            // SHA-512 digest), so it would be nice if that fit in the unrolled loop at least once.
+            // Plus, single-digit percentage performance differences might well be quite different
+            // on different hardware.
+
+            let input_u64 = read_u64(&input_chunk[0..]);
+
+            output_chunk[0] = encode_table[((input_u64 >> 58) & LOW_SIX_BITS) as usize];
+            output_chunk[1] = encode_table[((input_u64 >> 52) & LOW_SIX_BITS) as usize];
+            output_chunk[2] = encode_table[((input_u64 >> 46) & LOW_SIX_BITS) as usize];
+            output_chunk[3] = encode_table[((input_u64 >> 40) & LOW_SIX_BITS) as usize];
+            output_chunk[4] = encode_table[((input_u64 >> 34) & LOW_SIX_BITS) as usize];
+            output_chunk[5] = encode_table[((input_u64 >> 28) & LOW_SIX_BITS) as usize];
+            output_chunk[6] = encode_table[((input_u64 >> 22) & LOW_SIX_BITS) as usize];
+            output_chunk[7] = encode_table[((input_u64 >> 16) & LOW_SIX_BITS) as usize];
+
+            let input_u64 = read_u64(&input_chunk[6..]);
+
+            output_chunk[8] = encode_table[((input_u64 >> 58) & LOW_SIX_BITS) as usize];
+            output_chunk[9] = encode_table[((input_u64 >> 52) & LOW_SIX_BITS) as usize];
+            output_chunk[10] = encode_table[((input_u64 >> 46) & LOW_SIX_BITS) as usize];
+            output_chunk[11] = encode_table[((input_u64 >> 40) & LOW_SIX_BITS) as usize];
+            output_chunk[12] = encode_table[((input_u64 >> 34) & LOW_SIX_BITS) as usize];
+            output_chunk[13] = encode_table[((input_u64 >> 28) & LOW_SIX_BITS) as usize];
+            output_chunk[14] = encode_table[((input_u64 >> 22) & LOW_SIX_BITS) as usize];
+            output_chunk[15] = encode_table[((input_u64 >> 16) & LOW_SIX_BITS) as usize];
+
+            let input_u64 = read_u64(&input_chunk[12..]);
+
+            output_chunk[16] = encode_table[((input_u64 >> 58) & LOW_SIX_BITS) as usize];
+            output_chunk[17] = encode_table[((input_u64 >> 52) & LOW_SIX_BITS) as usize];
+            output_chunk[18] = encode_table[((input_u64 >> 46) & LOW_SIX_BITS) as usize];
+            output_chunk[19] = encode_table[((input_u64 >> 40) & LOW_SIX_BITS) as usize];
+            output_chunk[20] = encode_table[((input_u64 >> 34) & LOW_SIX_BITS) as usize];
+            output_chunk[21] = encode_table[((input_u64 >> 28) & LOW_SIX_BITS) as usize];
+            output_chunk[22] = encode_table[((input_u64 >> 22) & LOW_SIX_BITS) as usize];
+            output_chunk[23] = encode_table[((input_u64 >> 16) & LOW_SIX_BITS) as usize];
+
+            let input_u64 = read_u64(&input_chunk[18..]);
+
+            output_chunk[24] = encode_table[((input_u64 >> 58) & LOW_SIX_BITS) as usize];
+            output_chunk[25] = encode_table[((input_u64 >> 52) & LOW_SIX_BITS) as usize];
+            output_chunk[26] = encode_table[((input_u64 >> 46) & LOW_SIX_BITS) as usize];
+            output_chunk[27] = encode_table[((input_u64 >> 40) & LOW_SIX_BITS) as usize];
+            output_chunk[28] = encode_table[((input_u64 >> 34) & LOW_SIX_BITS) as usize];
+            output_chunk[29] = encode_table[((input_u64 >> 28) & LOW_SIX_BITS) as usize];
+            output_chunk[30] = encode_table[((input_u64 >> 22) & LOW_SIX_BITS) as usize];
+            output_chunk[31] = encode_table[((input_u64 >> 16) & LOW_SIX_BITS) as usize];
+
+            output_index += BLOCKS_PER_FAST_LOOP * 8;
+            input_index += BLOCKS_PER_FAST_LOOP * 6;
+        }
+    }
+
+    // Encode what's left after the fast loop.
+
+    const LOW_SIX_BITS_U8: u8 = 0x3F;
+
+    let rem = input.len() % 3;
+    let start_of_rem = input.len() - rem;
+
+    // start at the first index not handled by fast loop, which may be 0.
+
+    while input_index < start_of_rem {
+        let input_chunk = &input[input_index..(input_index + 3)];
+        let output_chunk = &mut output[output_index..(output_index + 4)];
+
+        output_chunk[0] = encode_table[(input_chunk[0] >> 2) as usize];
+        output_chunk[1] =
+            encode_table[((input_chunk[0] << 4 | input_chunk[1] >> 4) & LOW_SIX_BITS_U8) as usize];
+        output_chunk[2] =
+            encode_table[((input_chunk[1] << 2 | input_chunk[2] >> 6) & LOW_SIX_BITS_U8) as usize];
+        output_chunk[3] = encode_table[(input_chunk[2] & LOW_SIX_BITS_U8) as usize];
+
+        input_index += 3;
+        output_index += 4;
+    }
+
+    if rem == 2 {
+        output[output_index] = encode_table[(input[start_of_rem] >> 2) as usize];
+        output[output_index + 1] = encode_table[((input[start_of_rem] << 4
+            | input[start_of_rem + 1] >> 4)
+            & LOW_SIX_BITS_U8) as usize];
+        output[output_index + 2] =
+            encode_table[((input[start_of_rem + 1] << 2) & LOW_SIX_BITS_U8) as usize];
+        output_index += 3;
+    } else if rem == 1 {
+        output[output_index] = encode_table[(input[start_of_rem] >> 2) as usize];
+        output[output_index + 1] =
+            encode_table[((input[start_of_rem] << 4) & LOW_SIX_BITS_U8) as usize];
+        output_index += 2;
+    }
+
+    output_index
+}
+
+/// calculate the base64 encoded string size, including padding if appropriate
+pub fn encoded_size(bytes_len: usize, config: Config) -> Option<usize> {
+    let rem = bytes_len % 3;
+
+    let complete_input_chunks = bytes_len / 3;
+    let complete_chunk_output = complete_input_chunks.checked_mul(4);
+
+    if rem > 0 {
+        if config.pad {
+            complete_chunk_output.and_then(|c| c.checked_add(4))
+        } else {
+            let encoded_rem = match rem {
+                1 => 2,
+                2 => 3,
+                _ => unreachable!("Impossible remainder"),
+            };
+            complete_chunk_output.and_then(|c| c.checked_add(encoded_rem))
+        }
+    } else {
+        complete_chunk_output
+    }
+}
+
+/// Write padding characters.
+/// `output` is the slice where padding should be written, of length at least 2.
+///
+/// Returns the number of padding bytes written.
+pub fn add_padding(input_len: usize, output: &mut [u8]) -> usize {
+    let rem = input_len % 3;
+    let mut bytes_written = 0;
+    for _ in 0..((3 - rem) % 3) {
+        output[bytes_written] = PAD_BYTE;
+        bytes_written += 1;
+    }
+
+    bytes_written
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{
+        decode::decode_config_buf,
+        tests::{assert_encode_sanity, random_config},
+        Config, STANDARD, URL_SAFE_NO_PAD,
+    };
+
+    use rand::{
+        distributions::{Distribution, Uniform},
+        FromEntropy, Rng,
+    };
+    use std;
+    use std::str;
+
+    #[test]
+    fn encoded_size_correct_standard() {
+        assert_encoded_length(0, 0, STANDARD);
+
+        assert_encoded_length(1, 4, STANDARD);
+        assert_encoded_length(2, 4, STANDARD);
+        assert_encoded_length(3, 4, STANDARD);
+
+        assert_encoded_length(4, 8, STANDARD);
+        assert_encoded_length(5, 8, STANDARD);
+        assert_encoded_length(6, 8, STANDARD);
+
+        assert_encoded_length(7, 12, STANDARD);
+        assert_encoded_length(8, 12, STANDARD);
+        assert_encoded_length(9, 12, STANDARD);
+
+        assert_encoded_length(54, 72, STANDARD);
+
+        assert_encoded_length(55, 76, STANDARD);
+        assert_encoded_length(56, 76, STANDARD);
+        assert_encoded_length(57, 76, STANDARD);
+
+        assert_encoded_length(58, 80, STANDARD);
+    }
+
+    #[test]
+    fn encoded_size_correct_no_pad() {
+        assert_encoded_length(0, 0, URL_SAFE_NO_PAD);
+
+        assert_encoded_length(1, 2, URL_SAFE_NO_PAD);
+        assert_encoded_length(2, 3, URL_SAFE_NO_PAD);
+        assert_encoded_length(3, 4, URL_SAFE_NO_PAD);
+
+        assert_encoded_length(4, 6, URL_SAFE_NO_PAD);
+        assert_encoded_length(5, 7, URL_SAFE_NO_PAD);
+        assert_encoded_length(6, 8, URL_SAFE_NO_PAD);
+
+        assert_encoded_length(7, 10, URL_SAFE_NO_PAD);
+        assert_encoded_length(8, 11, URL_SAFE_NO_PAD);
+        assert_encoded_length(9, 12, URL_SAFE_NO_PAD);
+
+        assert_encoded_length(54, 72, URL_SAFE_NO_PAD);
+
+        assert_encoded_length(55, 74, URL_SAFE_NO_PAD);
+        assert_encoded_length(56, 75, URL_SAFE_NO_PAD);
+        assert_encoded_length(57, 76, URL_SAFE_NO_PAD);
+
+        assert_encoded_length(58, 78, URL_SAFE_NO_PAD);
+    }
+
+    #[test]
+    fn encoded_size_overflow() {
+        assert_eq!(None, encoded_size(std::usize::MAX, STANDARD));
+    }
+
+    #[test]
+    fn encode_config_buf_into_nonempty_buffer_doesnt_clobber_prefix() {
+        let mut orig_data = Vec::new();
+        let mut prefix = String::new();
+        let mut encoded_data_no_prefix = String::new();
+        let mut encoded_data_with_prefix = String::new();
+        let mut decoded = Vec::new();
+
+        let prefix_len_range = Uniform::new(0, 1000);
+        let input_len_range = Uniform::new(0, 1000);
+
+        let mut rng = rand::rngs::SmallRng::from_entropy();
+
+        for _ in 0..10_000 {
+            orig_data.clear();
+            prefix.clear();
+            encoded_data_no_prefix.clear();
+            encoded_data_with_prefix.clear();
+            decoded.clear();
+
+            let input_len = input_len_range.sample(&mut rng);
+
+            for _ in 0..input_len {
+                orig_data.push(rng.gen());
+            }
+
+            let prefix_len = prefix_len_range.sample(&mut rng);
+            for _ in 0..prefix_len {
+                // getting convenient random single-byte printable chars that aren't base64 is
+                // annoying
+                prefix.push('#');
+            }
+            encoded_data_with_prefix.push_str(&prefix);
+
+            let config = random_config(&mut rng);
+            encode_config_buf(&orig_data, config, &mut encoded_data_no_prefix);
+            encode_config_buf(&orig_data, config, &mut encoded_data_with_prefix);
+
+            assert_eq!(
+                encoded_data_no_prefix.len() + prefix_len,
+                encoded_data_with_prefix.len()
+            );
+            assert_encode_sanity(&encoded_data_no_prefix, config, input_len);
+            assert_encode_sanity(&encoded_data_with_prefix[prefix_len..], config, input_len);
+
+            // append plain encode onto prefix
+            prefix.push_str(&mut encoded_data_no_prefix);
+
+            assert_eq!(prefix, encoded_data_with_prefix);
+
+            decode_config_buf(&encoded_data_no_prefix, config, &mut decoded).unwrap();
+            assert_eq!(orig_data, decoded);
+        }
+    }
+
+    #[test]
+    fn encode_config_slice_into_nonempty_buffer_doesnt_clobber_suffix() {
+        let mut orig_data = Vec::new();
+        let mut encoded_data = Vec::new();
+        let mut encoded_data_original_state = Vec::new();
+        let mut decoded = Vec::new();
+
+        let input_len_range = Uniform::new(0, 1000);
+
+        let mut rng = rand::rngs::SmallRng::from_entropy();
+
+        for _ in 0..10_000 {
+            orig_data.clear();
+            encoded_data.clear();
+            encoded_data_original_state.clear();
+            decoded.clear();
+
+            let input_len = input_len_range.sample(&mut rng);
+
+            for _ in 0..input_len {
+                orig_data.push(rng.gen());
+            }
+
+            // plenty of existing garbage in the encoded buffer
+            for _ in 0..10 * input_len {
+                encoded_data.push(rng.gen());
+            }
+
+            encoded_data_original_state.extend_from_slice(&encoded_data);
+
+            let config = random_config(&mut rng);
+
+            let encoded_size = encoded_size(input_len, config).unwrap();
+
+            assert_eq!(
+                encoded_size,
+                encode_config_slice(&orig_data, config, &mut encoded_data)
+            );
+
+            assert_encode_sanity(
+                std::str::from_utf8(&encoded_data[0..encoded_size]).unwrap(),
+                config,
+                input_len,
+            );
+
+            assert_eq!(
+                &encoded_data[encoded_size..],
+                &encoded_data_original_state[encoded_size..]
+            );
+
+            decode_config_buf(&encoded_data[0..encoded_size], config, &mut decoded).unwrap();
+            assert_eq!(orig_data, decoded);
+        }
+    }
+
+    #[test]
+    fn encode_config_slice_fits_into_precisely_sized_slice() {
+        let mut orig_data = Vec::new();
+        let mut encoded_data = Vec::new();
+        let mut decoded = Vec::new();
+
+        let input_len_range = Uniform::new(0, 1000);
+
+        let mut rng = rand::rngs::SmallRng::from_entropy();
+
+        for _ in 0..10_000 {
+            orig_data.clear();
+            encoded_data.clear();
+            decoded.clear();
+
+            let input_len = input_len_range.sample(&mut rng);
+
+            for _ in 0..input_len {
+                orig_data.push(rng.gen());
+            }
+
+            let config = random_config(&mut rng);
+
+            let encoded_size = encoded_size(input_len, config).unwrap();
+
+            encoded_data.resize(encoded_size, 0);
+
+            assert_eq!(
+                encoded_size,
+                encode_config_slice(&orig_data, config, &mut encoded_data)
+            );
+
+            assert_encode_sanity(
+                std::str::from_utf8(&encoded_data[0..encoded_size]).unwrap(),
+                config,
+                input_len,
+            );
+
+            decode_config_buf(&encoded_data[0..encoded_size], config, &mut decoded).unwrap();
+            assert_eq!(orig_data, decoded);
+        }
+    }
+
+    #[test]
+    fn encode_to_slice_random_valid_utf8() {
+        let mut input = Vec::new();
+        let mut output = Vec::new();
+
+        let input_len_range = Uniform::new(0, 1000);
+
+        let mut rng = rand::rngs::SmallRng::from_entropy();
+
+        for _ in 0..10_000 {
+            input.clear();
+            output.clear();
+
+            let input_len = input_len_range.sample(&mut rng);
+
+            for _ in 0..input_len {
+                input.push(rng.gen());
+            }
+
+            let config = random_config(&mut rng);
+
+            // fill up the output buffer with garbage
+            let encoded_size = encoded_size(input_len, config).unwrap();
+            for _ in 0..encoded_size {
+                output.push(rng.gen());
+            }
+
+            let orig_output_buf = output.to_vec();
+
+            let bytes_written =
+                encode_to_slice(&input, &mut output, config.char_set.encode_table());
+
+            // make sure the part beyond bytes_written is the same garbage it was before
+            assert_eq!(orig_output_buf[bytes_written..], output[bytes_written..]);
+
+            // make sure the encoded bytes are UTF-8
+            let _ = str::from_utf8(&output[0..bytes_written]).unwrap();
+        }
+    }
+
+    #[test]
+    fn encode_with_padding_random_valid_utf8() {
+        let mut input = Vec::new();
+        let mut output = Vec::new();
+
+        let input_len_range = Uniform::new(0, 1000);
+
+        let mut rng = rand::rngs::SmallRng::from_entropy();
+
+        for _ in 0..10_000 {
+            input.clear();
+            output.clear();
+
+            let input_len = input_len_range.sample(&mut rng);
+
+            for _ in 0..input_len {
+                input.push(rng.gen());
+            }
+
+            let config = random_config(&mut rng);
+
+            // fill up the output buffer with garbage
+            let encoded_size = encoded_size(input_len, config).unwrap();
+            for _ in 0..encoded_size + 1000 {
+                output.push(rng.gen());
+            }
+
+            let orig_output_buf = output.to_vec();
+
+            encode_with_padding(&input, config, encoded_size, &mut output[0..encoded_size]);
+
+            // make sure the part beyond b64 is the same garbage it was before
+            assert_eq!(orig_output_buf[encoded_size..], output[encoded_size..]);
+
+            // make sure the encoded bytes are UTF-8
+            let _ = str::from_utf8(&output[0..encoded_size]).unwrap();
+        }
+    }
+
+    #[test]
+    fn add_padding_random_valid_utf8() {
+        let mut output = Vec::new();
+
+        let mut rng = rand::rngs::SmallRng::from_entropy();
+
+        // cover our bases for length % 3
+        for input_len in 0..10 {
+            output.clear();
+
+            // fill output with random
+            for _ in 0..10 {
+                output.push(rng.gen());
+            }
+
+            let orig_output_buf = output.to_vec();
+
+            let bytes_written = add_padding(input_len, &mut output);
+
+            // make sure the part beyond bytes_written is the same garbage it was before
+            assert_eq!(orig_output_buf[bytes_written..], output[bytes_written..]);
+
+            // make sure the encoded bytes are UTF-8
+            let _ = str::from_utf8(&output[0..bytes_written]).unwrap();
+        }
+    }
+
+    fn assert_encoded_length(input_len: usize, encoded_len: usize, config: Config) {
+        assert_eq!(encoded_len, encoded_size(input_len, config).unwrap());
+
+        let mut bytes: Vec<u8> = Vec::new();
+        let mut rng = rand::rngs::SmallRng::from_entropy();
+
+        for _ in 0..input_len {
+            bytes.push(rng.gen());
+        }
+
+        let encoded = encode_config(&bytes, config);
+        assert_encode_sanity(&encoded, config, input_len);
+
+        assert_eq!(encoded_len, encoded.len());
+    }
+
+    #[test]
+    fn encode_imap() {
+        assert_eq!(
+            encode_config(b"\xFB\xFF", crate::IMAP_MUTF7),
+            encode_config(b"\xFB\xFF", crate::STANDARD_NO_PAD).replace("/", ",")
+        );
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..6bded16
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,245 @@
+//! # Configs
+//!
+//! There isn't just one type of Base64; that would be too simple. You need to choose a character
+//! set (standard, URL-safe, etc) and padding suffix (yes/no).
+//! The `Config` struct encapsulates this info. There are some common configs included: `STANDARD`,
+//! `URL_SAFE`, etc. You can also make your own `Config` if needed.
+//!
+//! The functions that don't have `config` in the name (e.g. `encode()` and `decode()`) use the
+//! `STANDARD` config .
+//!
+//! The functions that write to a slice (the ones that end in `_slice`) are generally the fastest
+//! because they don't need to resize anything. If it fits in your workflow and you care about
+//! performance, keep using the same buffer (growing as need be) and use the `_slice` methods for
+//! the best performance.
+//!
+//! # Encoding
+//!
+//! Several different encoding functions are available to you depending on your desire for
+//! convenience vs performance.
+//!
+//! | Function                | Output                       | Allocates                      |
+//! | ----------------------- | ---------------------------- | ------------------------------ |
+//! | `encode`                | Returns a new `String`       | Always                         |
+//! | `encode_config`         | Returns a new `String`       | Always                         |
+//! | `encode_config_buf`     | Appends to provided `String` | Only if `String` needs to grow |
+//! | `encode_config_slice`   | Writes to provided `&[u8]`   | Never                          |
+//!
+//! All of the encoding functions that take a `Config` will pad as per the config.
+//!
+//! # Decoding
+//!
+//! Just as for encoding, there are different decoding functions available.
+//!
+//! | Function                | Output                        | Allocates                      |
+//! | ----------------------- | ----------------------------- | ------------------------------ |
+//! | `decode`                | Returns a new `Vec<u8>`       | Always                         |
+//! | `decode_config`         | Returns a new `Vec<u8>`       | Always                         |
+//! | `decode_config_buf`     | Appends to provided `Vec<u8>` | Only if `Vec` needs to grow    |
+//! | `decode_config_slice`   | Writes to provided `&[u8]`    | Never                          |
+//!
+//! Unlike encoding, where all possible input is valid, decoding can fail (see `DecodeError`).
+//!
+//! Input can be invalid because it has invalid characters or invalid padding. (No padding at all is
+//! valid, but excess padding is not.) Whitespace in the input is invalid.
+//!
+//! # `Read` and `Write`
+//!
+//! To map a `Read` of b64 bytes to the decoded bytes, wrap a reader (file, network socket, etc)
+//! with `base64::read::DecoderReader`. To write raw bytes and have them b64 encoded on the fly,
+//! wrap a writer with `base64::write::EncoderWriter`. There is some performance overhead (15% or
+//! so) because of the necessary buffer shuffling -- still fast enough that almost nobody cares.
+//! Also, these implementations do not heap allocate.
+//!
+//! # Panics
+//!
+//! If length calculations result in overflowing `usize`, a panic will result.
+//!
+//! The `_slice` flavors of encode or decode will panic if the provided output slice is too small,
+
+#![cfg_attr(feature = "cargo-clippy", allow(clippy::cast_lossless))]
+#![deny(
+    missing_docs,
+    trivial_casts,
+    trivial_numeric_casts,
+    unused_extern_crates,
+    unused_import_braces,
+    unused_results,
+    variant_size_differences,
+    warnings
+)]
+#![forbid(unsafe_code)]
+#![cfg_attr(not(any(feature = "std", test)), no_std)]
+
+#[cfg(all(feature = "alloc", not(any(feature = "std", test))))]
+extern crate alloc;
+#[cfg(any(feature = "std", test))]
+extern crate std as alloc;
+
+mod chunked_encoder;
+pub mod display;
+#[cfg(any(feature = "std", test))]
+pub mod read;
+mod tables;
+#[cfg(any(feature = "std", test))]
+pub mod write;
+
+mod encode;
+pub use crate::encode::encode_config_slice;
+#[cfg(any(feature = "alloc", feature = "std", test))]
+pub use crate::encode::{encode, encode_config, encode_config_buf};
+
+mod decode;
+#[cfg(any(feature = "alloc", feature = "std", test))]
+pub use crate::decode::{decode, decode_config, decode_config_buf};
+pub use crate::decode::{decode_config_slice, DecodeError};
+
+#[cfg(test)]
+mod tests;
+
+/// Available encoding character sets
+#[derive(Clone, Copy, Debug)]
+pub enum CharacterSet {
+    /// The standard character set (uses `+` and `/`).
+    ///
+    /// See [RFC 3548](https://tools.ietf.org/html/rfc3548#section-3).
+    Standard,
+    /// The URL safe character set (uses `-` and `_`).
+    ///
+    /// See [RFC 3548](https://tools.ietf.org/html/rfc3548#section-4).
+    UrlSafe,
+    /// The `crypt(3)` character set (uses `./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`).
+    ///
+    /// Not standardized, but folk wisdom on the net asserts that this alphabet is what crypt uses.
+    Crypt,
+    /// The bcrypt character set (uses `./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789`).
+    Bcrypt,
+    /// The character set used in IMAP-modified UTF-7 (uses `+` and `,`).
+    ///
+    /// See [RFC 3501](https://tools.ietf.org/html/rfc3501#section-5.1.3)
+    ImapMutf7,
+    /// The character set used in BinHex 4.0 files.
+    ///
+    /// See [BinHex 4.0 Definition](http://files.stairways.com/other/binhex-40-specs-info.txt)
+    BinHex,
+}
+
+impl CharacterSet {
+    fn encode_table(self) -> &'static [u8; 64] {
+        match self {
+            CharacterSet::Standard => tables::STANDARD_ENCODE,
+            CharacterSet::UrlSafe => tables::URL_SAFE_ENCODE,
+            CharacterSet::Crypt => tables::CRYPT_ENCODE,
+            CharacterSet::Bcrypt => tables::BCRYPT_ENCODE,
+            CharacterSet::ImapMutf7 => tables::IMAP_MUTF7_ENCODE,
+            CharacterSet::BinHex => tables::BINHEX_ENCODE,
+        }
+    }
+
+    fn decode_table(self) -> &'static [u8; 256] {
+        match self {
+            CharacterSet::Standard => tables::STANDARD_DECODE,
+            CharacterSet::UrlSafe => tables::URL_SAFE_DECODE,
+            CharacterSet::Crypt => tables::CRYPT_DECODE,
+            CharacterSet::Bcrypt => tables::BCRYPT_DECODE,
+            CharacterSet::ImapMutf7 => tables::IMAP_MUTF7_DECODE,
+            CharacterSet::BinHex => tables::BINHEX_DECODE,
+        }
+    }
+}
+
+/// Contains configuration parameters for base64 encoding
+#[derive(Clone, Copy, Debug)]
+pub struct Config {
+    /// Character set to use
+    char_set: CharacterSet,
+    /// True to pad output with `=` characters
+    pad: bool,
+    /// True to ignore excess nonzero bits in the last few symbols, otherwise an error is returned.
+    decode_allow_trailing_bits: bool,
+}
+
+impl Config {
+    /// Create a new `Config`.
+    pub const fn new(char_set: CharacterSet, pad: bool) -> Config {
+        Config {
+            char_set,
+            pad,
+            decode_allow_trailing_bits: false,
+        }
+    }
+
+    /// Sets whether to pad output with `=` characters.
+    pub const fn pad(self, pad: bool) -> Config {
+        Config { pad, ..self }
+    }
+
+    /// Sets whether to emit errors for nonzero trailing bits.
+    ///
+    /// This is useful when implementing
+    /// [forgiving-base64 decode](https://infra.spec.whatwg.org/#forgiving-base64-decode).
+    pub const fn decode_allow_trailing_bits(self, allow: bool) -> Config {
+        Config {
+            decode_allow_trailing_bits: allow,
+            ..self
+        }
+    }
+}
+
+/// Standard character set with padding.
+pub const STANDARD: Config = Config {
+    char_set: CharacterSet::Standard,
+    pad: true,
+    decode_allow_trailing_bits: false,
+};
+
+/// Standard character set without padding.
+pub const STANDARD_NO_PAD: Config = Config {
+    char_set: CharacterSet::Standard,
+    pad: false,
+    decode_allow_trailing_bits: false,
+};
+
+/// URL-safe character set with padding
+pub const URL_SAFE: Config = Config {
+    char_set: CharacterSet::UrlSafe,
+    pad: true,
+    decode_allow_trailing_bits: false,
+};
+
+/// URL-safe character set without padding
+pub const URL_SAFE_NO_PAD: Config = Config {
+    char_set: CharacterSet::UrlSafe,
+    pad: false,
+    decode_allow_trailing_bits: false,
+};
+
+/// As per `crypt(3)` requirements
+pub const CRYPT: Config = Config {
+    char_set: CharacterSet::Crypt,
+    pad: false,
+    decode_allow_trailing_bits: false,
+};
+
+/// Bcrypt character set
+pub const BCRYPT: Config = Config {
+    char_set: CharacterSet::Bcrypt,
+    pad: false,
+    decode_allow_trailing_bits: false,
+};
+
+/// IMAP modified UTF-7 requirements
+pub const IMAP_MUTF7: Config = Config {
+    char_set: CharacterSet::ImapMutf7,
+    pad: false,
+    decode_allow_trailing_bits: false,
+};
+
+/// BinHex character set
+pub const BINHEX: Config = Config {
+    char_set: CharacterSet::BinHex,
+    pad: false,
+    decode_allow_trailing_bits: false,
+};
+
+const PAD_BYTE: u8 = b'=';
diff --git a/src/read/decoder.rs b/src/read/decoder.rs
new file mode 100644
index 0000000..7a9c4cd
--- /dev/null
+++ b/src/read/decoder.rs
@@ -0,0 +1,282 @@
+use crate::{decode_config_slice, Config, DecodeError};
+use std::io::Read;
+use std::{cmp, fmt, io};
+
+// This should be large, but it has to fit on the stack.
+pub(crate) const BUF_SIZE: usize = 1024;
+
+// 4 bytes of base64 data encode 3 bytes of raw data (modulo padding).
+const BASE64_CHUNK_SIZE: usize = 4;
+const DECODED_CHUNK_SIZE: usize = 3;
+
+/// A `Read` implementation that decodes base64 data read from an underlying reader.
+///
+/// # Examples
+///
+/// ```
+/// use std::io::Read;
+/// use std::io::Cursor;
+///
+/// // use a cursor as the simplest possible `Read` -- in real code this is probably a file, etc.
+/// let mut wrapped_reader = Cursor::new(b"YXNkZg==");
+/// let mut decoder = base64::read::DecoderReader::new(
+///     &mut wrapped_reader, base64::STANDARD);
+///
+/// // handle errors as you normally would
+/// let mut result = Vec::new();
+/// decoder.read_to_end(&mut result).unwrap();
+///
+/// assert_eq!(b"asdf", &result[..]);
+///
+/// ```
+pub struct DecoderReader<'a, R: 'a + io::Read> {
+    config: Config,
+    /// Where b64 data is read from
+    r: &'a mut R,
+
+    // Holds b64 data read from the delegate reader.
+    b64_buffer: [u8; BUF_SIZE],
+    // The start of the pending buffered data in b64_buffer.
+    b64_offset: usize,
+    // The amount of buffered b64 data.
+    b64_len: usize,
+    // Since the caller may provide us with a buffer of size 1 or 2 that's too small to copy a
+    // decoded chunk in to, we have to be able to hang on to a few decoded bytes.
+    // Technically we only need to hold 2 bytes but then we'd need a separate temporary buffer to
+    // decode 3 bytes into and then juggle copying one byte into the provided read buf and the rest
+    // into here, which seems like a lot of complexity for 1 extra byte of storage.
+    decoded_buffer: [u8; 3],
+    // index of start of decoded data
+    decoded_offset: usize,
+    // length of decoded data
+    decoded_len: usize,
+    // used to provide accurate offsets in errors
+    total_b64_decoded: usize,
+}
+
+impl<'a, R: io::Read> fmt::Debug for DecoderReader<'a, R> {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.debug_struct("DecoderReader")
+            .field("config", &self.config)
+            .field("b64_offset", &self.b64_offset)
+            .field("b64_len", &self.b64_len)
+            .field("decoded_buffer", &self.decoded_buffer)
+            .field("decoded_offset", &self.decoded_offset)
+            .field("decoded_len", &self.decoded_len)
+            .field("total_b64_decoded", &self.total_b64_decoded)
+            .finish()
+    }
+}
+
+impl<'a, R: io::Read> DecoderReader<'a, R> {
+    /// Create a new decoder that will read from the provided reader `r`.
+    pub fn new(r: &'a mut R, config: Config) -> Self {
+        DecoderReader {
+            config,
+            r,
+            b64_buffer: [0; BUF_SIZE],
+            b64_offset: 0,
+            b64_len: 0,
+            decoded_buffer: [0; DECODED_CHUNK_SIZE],
+            decoded_offset: 0,
+            decoded_len: 0,
+            total_b64_decoded: 0,
+        }
+    }
+
+    /// Write as much as possible of the decoded buffer into the target buffer.
+    /// Must only be called when there is something to write and space to write into.
+    /// Returns a Result with the number of (decoded) bytes copied.
+    fn flush_decoded_buf(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+        debug_assert!(self.decoded_len > 0);
+        debug_assert!(buf.len() > 0);
+
+        let copy_len = cmp::min(self.decoded_len, buf.len());
+        debug_assert!(copy_len > 0);
+        debug_assert!(copy_len <= self.decoded_len);
+
+        buf[..copy_len].copy_from_slice(
+            &self.decoded_buffer[self.decoded_offset..self.decoded_offset + copy_len],
+        );
+
+        self.decoded_offset += copy_len;
+        self.decoded_len -= copy_len;
+
+        debug_assert!(self.decoded_len < DECODED_CHUNK_SIZE);
+
+        Ok(copy_len)
+    }
+
+    /// Read into the remaining space in the buffer after the current contents.
+    /// Must only be called when there is space to read into in the buffer.
+    /// Returns the number of bytes read.
+    fn read_from_delegate(&mut self) -> io::Result<usize> {
+        debug_assert!(self.b64_offset + self.b64_len < BUF_SIZE);
+
+        let read = self
+            .r
+            .read(&mut self.b64_buffer[self.b64_offset + self.b64_len..])?;
+        self.b64_len += read;
+
+        debug_assert!(self.b64_offset + self.b64_len <= BUF_SIZE);
+
+        return Ok(read);
+    }
+
+    /// Decode the requested number of bytes from the b64 buffer into the provided buffer. It's the
+    /// caller's responsibility to choose the number of b64 bytes to decode correctly.
+    ///
+    /// Returns a Result with the number of decoded bytes written to `buf`.
+    fn decode_to_buf(&mut self, num_bytes: usize, buf: &mut [u8]) -> io::Result<usize> {
+        debug_assert!(self.b64_len >= num_bytes);
+        debug_assert!(self.b64_offset + self.b64_len <= BUF_SIZE);
+        debug_assert!(buf.len() > 0);
+
+        let decoded = decode_config_slice(
+            &self.b64_buffer[self.b64_offset..self.b64_offset + num_bytes],
+            self.config,
+            &mut buf[..],
+        )
+        .map_err(|e| match e {
+            DecodeError::InvalidByte(offset, byte) => {
+                DecodeError::InvalidByte(self.total_b64_decoded + offset, byte)
+            }
+            DecodeError::InvalidLength => DecodeError::InvalidLength,
+            DecodeError::InvalidLastSymbol(offset, byte) => {
+                DecodeError::InvalidLastSymbol(self.total_b64_decoded + offset, byte)
+            }
+        })
+        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
+
+        self.total_b64_decoded += num_bytes;
+        self.b64_offset += num_bytes;
+        self.b64_len -= num_bytes;
+
+        debug_assert!(self.b64_offset + self.b64_len <= BUF_SIZE);
+
+        Ok(decoded)
+    }
+}
+
+impl<'a, R: Read> Read for DecoderReader<'a, R> {
+    /// Decode input from the wrapped reader.
+    ///
+    /// Under non-error circumstances, this returns `Ok` with the value being the number of bytes
+    /// written in `buf`.
+    ///
+    /// Where possible, this function buffers base64 to minimize the number of read() calls to the
+    /// delegate reader.
+    ///
+    /// # Errors
+    ///
+    /// Any errors emitted by the delegate reader are returned. Decoding errors due to invalid
+    /// base64 are also possible, and will have `io::ErrorKind::InvalidData`.
+    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+        if buf.len() == 0 {
+            return Ok(0);
+        }
+
+        // offset == BUF_SIZE when we copied it all last time
+        debug_assert!(self.b64_offset <= BUF_SIZE);
+        debug_assert!(self.b64_offset + self.b64_len <= BUF_SIZE);
+        debug_assert!(if self.b64_offset == BUF_SIZE {
+            self.b64_len == 0
+        } else {
+            self.b64_len <= BUF_SIZE
+        });
+
+        debug_assert!(if self.decoded_len == 0 {
+            // can be = when we were able to copy the complete chunk
+            self.decoded_offset <= DECODED_CHUNK_SIZE
+        } else {
+            self.decoded_offset < DECODED_CHUNK_SIZE
+        });
+
+        // We shouldn't ever decode into here when we can't immediately write at least one byte into
+        // the provided buf, so the effective length should only be 3 momentarily between when we
+        // decode and when we copy into the target buffer.
+        debug_assert!(self.decoded_len < DECODED_CHUNK_SIZE);
+        debug_assert!(self.decoded_len + self.decoded_offset <= DECODED_CHUNK_SIZE);
+
+        if self.decoded_len > 0 {
+            // we have a few leftover decoded bytes; flush that rather than pull in more b64
+            self.flush_decoded_buf(buf)
+        } else {
+            let mut at_eof = false;
+            while self.b64_len < BASE64_CHUNK_SIZE {
+                // Work around lack of copy_within, which is only present in 1.37
+                // Copy any bytes we have to the start of the buffer.
+                // We know we have < 1 chunk, so we can use a tiny tmp buffer.
+                let mut memmove_buf = [0_u8; BASE64_CHUNK_SIZE];
+                memmove_buf[..self.b64_len].copy_from_slice(
+                    &self.b64_buffer[self.b64_offset..self.b64_offset + self.b64_len],
+                );
+                self.b64_buffer[0..self.b64_len].copy_from_slice(&memmove_buf[..self.b64_len]);
+                self.b64_offset = 0;
+
+                // then fill in more data
+                let read = self.read_from_delegate()?;
+                if read == 0 {
+                    // we never pass in an empty buf, so 0 => we've hit EOF
+                    at_eof = true;
+                    break;
+                }
+            }
+
+            if self.b64_len == 0 {
+                debug_assert!(at_eof);
+                // we must be at EOF, and we have no data left to decode
+                return Ok(0);
+            };
+
+            debug_assert!(if at_eof {
+                // if we are at eof, we may not have a complete chunk
+                self.b64_len > 0
+            } else {
+                // otherwise, we must have at least one chunk
+                self.b64_len >= BASE64_CHUNK_SIZE
+            });
+
+            debug_assert_eq!(0, self.decoded_len);
+
+            if buf.len() < DECODED_CHUNK_SIZE {
+                // caller requested an annoyingly short read
+                // have to write to a tmp buf first to avoid double mutable borrow
+                let mut decoded_chunk = [0_u8; DECODED_CHUNK_SIZE];
+                // if we are at eof, could have less than BASE64_CHUNK_SIZE, in which case we have
+                // to assume that these last few tokens are, in fact, valid (i.e. must be 2-4 b64
+                // tokens, not 1, since 1 token can't decode to 1 byte).
+                let to_decode = cmp::min(self.b64_len, BASE64_CHUNK_SIZE);
+
+                let decoded = self.decode_to_buf(to_decode, &mut decoded_chunk[..])?;
+                self.decoded_buffer[..decoded].copy_from_slice(&decoded_chunk[..decoded]);
+
+                self.decoded_offset = 0;
+                self.decoded_len = decoded;
+
+                // can be less than 3 on last block due to padding
+                debug_assert!(decoded <= 3);
+
+                self.flush_decoded_buf(buf)
+            } else {
+                let b64_bytes_that_can_decode_into_buf = (buf.len() / DECODED_CHUNK_SIZE)
+                    .checked_mul(BASE64_CHUNK_SIZE)
+                    .expect("too many chunks");
+                debug_assert!(b64_bytes_that_can_decode_into_buf >= BASE64_CHUNK_SIZE);
+
+                let b64_bytes_available_to_decode = if at_eof {
+                    self.b64_len
+                } else {
+                    // only use complete chunks
+                    self.b64_len - self.b64_len % 4
+                };
+
+                let actual_decode_len = cmp::min(
+                    b64_bytes_that_can_decode_into_buf,
+                    b64_bytes_available_to_decode,
+                );
+                self.decode_to_buf(actual_decode_len, buf)
+            }
+        }
+    }
+}
diff --git a/src/read/decoder_tests.rs b/src/read/decoder_tests.rs
new file mode 100644
index 0000000..265d423
--- /dev/null
+++ b/src/read/decoder_tests.rs
@@ -0,0 +1,335 @@
+use std::io::{self, Read};
+
+use rand::{Rng, RngCore};
+use std::{cmp, iter};
+
+use super::decoder::{DecoderReader, BUF_SIZE};
+use crate::encode::encode_config_buf;
+use crate::tests::random_config;
+use crate::{decode_config_buf, DecodeError, STANDARD};
+
+#[test]
+fn simple() {
+    let tests: &[(&[u8], &[u8])] = &[
+        (&b"0"[..], &b"MA=="[..]),
+        (b"01", b"MDE="),
+        (b"012", b"MDEy"),
+        (b"0123", b"MDEyMw=="),
+        (b"01234", b"MDEyMzQ="),
+        (b"012345", b"MDEyMzQ1"),
+        (b"0123456", b"MDEyMzQ1Ng=="),
+        (b"01234567", b"MDEyMzQ1Njc="),
+        (b"012345678", b"MDEyMzQ1Njc4"),
+        (b"0123456789", b"MDEyMzQ1Njc4OQ=="),
+    ][..];
+
+    for (text_expected, base64data) in tests.iter() {
+        // Read n bytes at a time.
+        for n in 1..base64data.len() + 1 {
+            let mut wrapped_reader = io::Cursor::new(base64data);
+            let mut decoder = DecoderReader::new(&mut wrapped_reader, STANDARD);
+
+            // handle errors as you normally would
+            let mut text_got = Vec::new();
+            let mut buffer = vec![0u8; n];
+            while let Ok(read) = decoder.read(&mut buffer[..]) {
+                if read == 0 {
+                    break;
+                }
+                text_got.extend_from_slice(&buffer[..read]);
+            }
+
+            assert_eq!(
+                text_got,
+                *text_expected,
+                "\nGot: {}\nExpected: {}",
+                String::from_utf8_lossy(&text_got[..]),
+                String::from_utf8_lossy(text_expected)
+            );
+        }
+    }
+}
+
+// Make sure we error out on trailing junk.
+#[test]
+fn trailing_junk() {
+    let tests: &[&[u8]] = &[&b"MDEyMzQ1Njc4*!@#$%^&"[..], b"MDEyMzQ1Njc4OQ== "][..];
+
+    for base64data in tests.iter() {
+        // Read n bytes at a time.
+        for n in 1..base64data.len() + 1 {
+            let mut wrapped_reader = io::Cursor::new(base64data);
+            let mut decoder = DecoderReader::new(&mut wrapped_reader, STANDARD);
+
+            // handle errors as you normally would
+            let mut buffer = vec![0u8; n];
+            let mut saw_error = false;
+            loop {
+                match decoder.read(&mut buffer[..]) {
+                    Err(_) => {
+                        saw_error = true;
+                        break;
+                    }
+                    Ok(read) if read == 0 => break,
+                    Ok(_) => (),
+                }
+            }
+
+            assert!(saw_error);
+        }
+    }
+}
+
+#[test]
+fn handles_short_read_from_delegate() {
+    let mut rng = rand::thread_rng();
+    let mut bytes = Vec::new();
+    let mut b64 = String::new();
+    let mut decoded = Vec::new();
+
+    for _ in 0..10_000 {
+        bytes.clear();
+        b64.clear();
+        decoded.clear();
+
+        let size = rng.gen_range(0, 10 * BUF_SIZE);
+        bytes.extend(iter::repeat(0).take(size));
+        bytes.truncate(size);
+        rng.fill_bytes(&mut bytes[..size]);
+        assert_eq!(size, bytes.len());
+
+        let config = random_config(&mut rng);
+        encode_config_buf(&bytes[..], config, &mut b64);
+
+        let mut wrapped_reader = io::Cursor::new(b64.as_bytes());
+        let mut short_reader = RandomShortRead {
+            delegate: &mut wrapped_reader,
+            rng: &mut rng,
+        };
+
+        let mut decoder = DecoderReader::new(&mut short_reader, config);
+
+        let decoded_len = decoder.read_to_end(&mut decoded).unwrap();
+        assert_eq!(size, decoded_len);
+        assert_eq!(&bytes[..], &decoded[..]);
+    }
+}
+
+#[test]
+fn read_in_short_increments() {
+    let mut rng = rand::thread_rng();
+    let mut bytes = Vec::new();
+    let mut b64 = String::new();
+    let mut decoded = Vec::new();
+
+    for _ in 0..10_000 {
+        bytes.clear();
+        b64.clear();
+        decoded.clear();
+
+        let size = rng.gen_range(0, 10 * BUF_SIZE);
+        bytes.extend(iter::repeat(0).take(size));
+        // leave room to play around with larger buffers
+        decoded.extend(iter::repeat(0).take(size * 3));
+
+        rng.fill_bytes(&mut bytes[..]);
+        assert_eq!(size, bytes.len());
+
+        let config = random_config(&mut rng);
+
+        encode_config_buf(&bytes[..], config, &mut b64);
+
+        let mut wrapped_reader = io::Cursor::new(&b64[..]);
+        let mut decoder = DecoderReader::new(&mut wrapped_reader, config);
+
+        consume_with_short_reads_and_validate(&mut rng, &bytes[..], &mut decoded, &mut decoder);
+    }
+}
+
+#[test]
+fn read_in_short_increments_with_short_delegate_reads() {
+    let mut rng = rand::thread_rng();
+    let mut bytes = Vec::new();
+    let mut b64 = String::new();
+    let mut decoded = Vec::new();
+
+    for _ in 0..10_000 {
+        bytes.clear();
+        b64.clear();
+        decoded.clear();
+
+        let size = rng.gen_range(0, 10 * BUF_SIZE);
+        bytes.extend(iter::repeat(0).take(size));
+        // leave room to play around with larger buffers
+        decoded.extend(iter::repeat(0).take(size * 3));
+
+        rng.fill_bytes(&mut bytes[..]);
+        assert_eq!(size, bytes.len());
+
+        let config = random_config(&mut rng);
+
+        encode_config_buf(&bytes[..], config, &mut b64);
+
+        let mut base_reader = io::Cursor::new(&b64[..]);
+        let mut decoder = DecoderReader::new(&mut base_reader, config);
+        let mut short_reader = RandomShortRead {
+            delegate: &mut decoder,
+            rng: &mut rand::thread_rng(),
+        };
+
+        consume_with_short_reads_and_validate(&mut rng, &bytes[..], &mut decoded, &mut short_reader)
+    }
+}
+
+#[test]
+fn reports_invalid_last_symbol_correctly() {
+    let mut rng = rand::thread_rng();
+    let mut bytes = Vec::new();
+    let mut b64 = String::new();
+    let mut b64_bytes = Vec::new();
+    let mut decoded = Vec::new();
+    let mut bulk_decoded = Vec::new();
+
+    for _ in 0..1_000 {
+        bytes.clear();
+        b64.clear();
+        b64_bytes.clear();
+
+        let size = rng.gen_range(1, 10 * BUF_SIZE);
+        bytes.extend(iter::repeat(0).take(size));
+        decoded.extend(iter::repeat(0).take(size));
+        rng.fill_bytes(&mut bytes[..]);
+        assert_eq!(size, bytes.len());
+
+        let mut config = random_config(&mut rng);
+        // changing padding will cause invalid padding errors when we twiddle the last byte
+        config.pad = false;
+
+        encode_config_buf(&bytes[..], config, &mut b64);
+        b64_bytes.extend(b64.bytes());
+        assert_eq!(b64_bytes.len(), b64.len());
+
+        // change the last character to every possible symbol. Should behave the same as bulk
+        // decoding whether invalid or valid.
+        for &s1 in config.char_set.encode_table().iter() {
+            decoded.clear();
+            bulk_decoded.clear();
+
+            // replace the last
+            *b64_bytes.last_mut().unwrap() = s1;
+            let bulk_res = decode_config_buf(&b64_bytes[..], config, &mut bulk_decoded);
+
+            let mut wrapped_reader = io::Cursor::new(&b64_bytes[..]);
+            let mut decoder = DecoderReader::new(&mut wrapped_reader, config);
+
+            let stream_res = decoder.read_to_end(&mut decoded).map(|_| ()).map_err(|e| {
+                e.into_inner()
+                    .and_then(|e| e.downcast::<DecodeError>().ok())
+            });
+
+            assert_eq!(bulk_res.map_err(|e| Some(Box::new(e))), stream_res);
+        }
+    }
+}
+
+#[test]
+fn reports_invalid_byte_correctly() {
+    let mut rng = rand::thread_rng();
+    let mut bytes = Vec::new();
+    let mut b64 = String::new();
+    let mut decoded = Vec::new();
+
+    for _ in 0..10_000 {
+        bytes.clear();
+        b64.clear();
+        decoded.clear();
+
+        let size = rng.gen_range(1, 10 * BUF_SIZE);
+        bytes.extend(iter::repeat(0).take(size));
+        rng.fill_bytes(&mut bytes[..size]);
+        assert_eq!(size, bytes.len());
+
+        let config = random_config(&mut rng);
+        encode_config_buf(&bytes[..], config, &mut b64);
+        // replace one byte, somewhere, with '*', which is invalid
+        let bad_byte_pos = rng.gen_range(0, &b64.len());
+        let mut b64_bytes = b64.bytes().collect::<Vec<u8>>();
+        b64_bytes[bad_byte_pos] = b'*';
+
+        let mut wrapped_reader = io::Cursor::new(b64_bytes.clone());
+        let mut decoder = DecoderReader::new(&mut wrapped_reader, config);
+
+        // some gymnastics to avoid double-moving the io::Error, which is not Copy
+        let read_decode_err = decoder
+            .read_to_end(&mut decoded)
+            .map_err(|e| {
+                let kind = e.kind();
+                let inner = e
+                    .into_inner()
+                    .and_then(|e| e.downcast::<DecodeError>().ok());
+                inner.map(|i| (*i, kind))
+            })
+            .err()
+            .and_then(|o| o);
+
+        let mut bulk_buf = Vec::new();
+        let bulk_decode_err = decode_config_buf(&b64_bytes[..], config, &mut bulk_buf).err();
+
+        // it's tricky to predict where the invalid data's offset will be since if it's in the last
+        // chunk it will be reported at the first padding location because it's treated as invalid
+        // padding. So, we just check that it's the same as it is for decoding all at once.
+        assert_eq!(
+            bulk_decode_err.map(|e| (e, io::ErrorKind::InvalidData)),
+            read_decode_err
+        );
+    }
+}
+
+fn consume_with_short_reads_and_validate<R: Read>(
+    rng: &mut rand::rngs::ThreadRng,
+    expected_bytes: &[u8],
+    decoded: &mut Vec<u8>,
+    short_reader: &mut R,
+) -> () {
+    let mut total_read = 0_usize;
+    loop {
+        assert!(
+            total_read <= expected_bytes.len(),
+            "tr {} size {}",
+            total_read,
+            expected_bytes.len()
+        );
+        if total_read == expected_bytes.len() {
+            assert_eq!(expected_bytes, &decoded[..total_read]);
+            // should be done
+            assert_eq!(0, short_reader.read(&mut decoded[..]).unwrap());
+            // didn't write anything
+            assert_eq!(expected_bytes, &decoded[..total_read]);
+
+            break;
+        }
+        let decode_len = rng.gen_range(1, cmp::max(2, expected_bytes.len() * 2));
+
+        let read = short_reader
+            .read(&mut decoded[total_read..total_read + decode_len])
+            .unwrap();
+        total_read += read;
+    }
+}
+
+/// Limits how many bytes a reader will provide in each read call.
+/// Useful for shaking out code that may work fine only with typical input sources that always fill
+/// the buffer.
+struct RandomShortRead<'a, 'b, R: io::Read, N: rand::Rng> {
+    delegate: &'b mut R,
+    rng: &'a mut N,
+}
+
+impl<'a, 'b, R: io::Read, N: rand::Rng> io::Read for RandomShortRead<'a, 'b, R, N> {
+    fn read(&mut self, buf: &mut [u8]) -> Result<usize, io::Error> {
+        // avoid 0 since it means EOF for non-empty buffers
+        let effective_len = cmp::min(self.rng.gen_range(1, 20), buf.len());
+
+        self.delegate.read(&mut buf[..effective_len])
+    }
+}
diff --git a/src/read/mod.rs b/src/read/mod.rs
new file mode 100644
index 0000000..8560644
--- /dev/null
+++ b/src/read/mod.rs
@@ -0,0 +1,6 @@
+//! Implementations of `io::Read` to transparently decode base64.
+mod decoder;
+pub use self::decoder::DecoderReader;
+
+#[cfg(test)]
+mod decoder_tests;
diff --git a/src/tables.rs b/src/tables.rs
new file mode 100644
index 0000000..a45851c
--- /dev/null
+++ b/src/tables.rs
@@ -0,0 +1,1957 @@
+pub const INVALID_VALUE: u8 = 255;
+#[rustfmt::skip]
+pub const STANDARD_ENCODE: &[u8; 64] = &[
+    65, // input 0 (0x0) => 'A' (0x41)
+    66, // input 1 (0x1) => 'B' (0x42)
+    67, // input 2 (0x2) => 'C' (0x43)
+    68, // input 3 (0x3) => 'D' (0x44)
+    69, // input 4 (0x4) => 'E' (0x45)
+    70, // input 5 (0x5) => 'F' (0x46)
+    71, // input 6 (0x6) => 'G' (0x47)
+    72, // input 7 (0x7) => 'H' (0x48)
+    73, // input 8 (0x8) => 'I' (0x49)
+    74, // input 9 (0x9) => 'J' (0x4A)
+    75, // input 10 (0xA) => 'K' (0x4B)
+    76, // input 11 (0xB) => 'L' (0x4C)
+    77, // input 12 (0xC) => 'M' (0x4D)
+    78, // input 13 (0xD) => 'N' (0x4E)
+    79, // input 14 (0xE) => 'O' (0x4F)
+    80, // input 15 (0xF) => 'P' (0x50)
+    81, // input 16 (0x10) => 'Q' (0x51)
+    82, // input 17 (0x11) => 'R' (0x52)
+    83, // input 18 (0x12) => 'S' (0x53)
+    84, // input 19 (0x13) => 'T' (0x54)
+    85, // input 20 (0x14) => 'U' (0x55)
+    86, // input 21 (0x15) => 'V' (0x56)
+    87, // input 22 (0x16) => 'W' (0x57)
+    88, // input 23 (0x17) => 'X' (0x58)
+    89, // input 24 (0x18) => 'Y' (0x59)
+    90, // input 25 (0x19) => 'Z' (0x5A)
+    97, // input 26 (0x1A) => 'a' (0x61)
+    98, // input 27 (0x1B) => 'b' (0x62)
+    99, // input 28 (0x1C) => 'c' (0x63)
+    100, // input 29 (0x1D) => 'd' (0x64)
+    101, // input 30 (0x1E) => 'e' (0x65)
+    102, // input 31 (0x1F) => 'f' (0x66)
+    103, // input 32 (0x20) => 'g' (0x67)
+    104, // input 33 (0x21) => 'h' (0x68)
+    105, // input 34 (0x22) => 'i' (0x69)
+    106, // input 35 (0x23) => 'j' (0x6A)
+    107, // input 36 (0x24) => 'k' (0x6B)
+    108, // input 37 (0x25) => 'l' (0x6C)
+    109, // input 38 (0x26) => 'm' (0x6D)
+    110, // input 39 (0x27) => 'n' (0x6E)
+    111, // input 40 (0x28) => 'o' (0x6F)
+    112, // input 41 (0x29) => 'p' (0x70)
+    113, // input 42 (0x2A) => 'q' (0x71)
+    114, // input 43 (0x2B) => 'r' (0x72)
+    115, // input 44 (0x2C) => 's' (0x73)
+    116, // input 45 (0x2D) => 't' (0x74)
+    117, // input 46 (0x2E) => 'u' (0x75)
+    118, // input 47 (0x2F) => 'v' (0x76)
+    119, // input 48 (0x30) => 'w' (0x77)
+    120, // input 49 (0x31) => 'x' (0x78)
+    121, // input 50 (0x32) => 'y' (0x79)
+    122, // input 51 (0x33) => 'z' (0x7A)
+    48, // input 52 (0x34) => '0' (0x30)
+    49, // input 53 (0x35) => '1' (0x31)
+    50, // input 54 (0x36) => '2' (0x32)
+    51, // input 55 (0x37) => '3' (0x33)
+    52, // input 56 (0x38) => '4' (0x34)
+    53, // input 57 (0x39) => '5' (0x35)
+    54, // input 58 (0x3A) => '6' (0x36)
+    55, // input 59 (0x3B) => '7' (0x37)
+    56, // input 60 (0x3C) => '8' (0x38)
+    57, // input 61 (0x3D) => '9' (0x39)
+    43, // input 62 (0x3E) => '+' (0x2B)
+    47, // input 63 (0x3F) => '/' (0x2F)
+];
+#[rustfmt::skip]
+pub const STANDARD_DECODE: &[u8; 256] = &[
+    INVALID_VALUE, // input 0 (0x0)
+    INVALID_VALUE, // input 1 (0x1)
+    INVALID_VALUE, // input 2 (0x2)
+    INVALID_VALUE, // input 3 (0x3)
+    INVALID_VALUE, // input 4 (0x4)
+    INVALID_VALUE, // input 5 (0x5)
+    INVALID_VALUE, // input 6 (0x6)
+    INVALID_VALUE, // input 7 (0x7)
+    INVALID_VALUE, // input 8 (0x8)
+    INVALID_VALUE, // input 9 (0x9)
+    INVALID_VALUE, // input 10 (0xA)
+    INVALID_VALUE, // input 11 (0xB)
+    INVALID_VALUE, // input 12 (0xC)
+    INVALID_VALUE, // input 13 (0xD)
+    INVALID_VALUE, // input 14 (0xE)
+    INVALID_VALUE, // input 15 (0xF)
+    INVALID_VALUE, // input 16 (0x10)
+    INVALID_VALUE, // input 17 (0x11)
+    INVALID_VALUE, // input 18 (0x12)
+    INVALID_VALUE, // input 19 (0x13)
+    INVALID_VALUE, // input 20 (0x14)
+    INVALID_VALUE, // input 21 (0x15)
+    INVALID_VALUE, // input 22 (0x16)
+    INVALID_VALUE, // input 23 (0x17)
+    INVALID_VALUE, // input 24 (0x18)
+    INVALID_VALUE, // input 25 (0x19)
+    INVALID_VALUE, // input 26 (0x1A)
+    INVALID_VALUE, // input 27 (0x1B)
+    INVALID_VALUE, // input 28 (0x1C)
+    INVALID_VALUE, // input 29 (0x1D)
+    INVALID_VALUE, // input 30 (0x1E)
+    INVALID_VALUE, // input 31 (0x1F)
+    INVALID_VALUE, // input 32 (0x20)
+    INVALID_VALUE, // input 33 (0x21)
+    INVALID_VALUE, // input 34 (0x22)
+    INVALID_VALUE, // input 35 (0x23)
+    INVALID_VALUE, // input 36 (0x24)
+    INVALID_VALUE, // input 37 (0x25)
+    INVALID_VALUE, // input 38 (0x26)
+    INVALID_VALUE, // input 39 (0x27)
+    INVALID_VALUE, // input 40 (0x28)
+    INVALID_VALUE, // input 41 (0x29)
+    INVALID_VALUE, // input 42 (0x2A)
+    62, // input 43 (0x2B char '+') => 62 (0x3E)
+    INVALID_VALUE, // input 44 (0x2C)
+    INVALID_VALUE, // input 45 (0x2D)
+    INVALID_VALUE, // input 46 (0x2E)
+    63, // input 47 (0x2F char '/') => 63 (0x3F)
+    52, // input 48 (0x30 char '0') => 52 (0x34)
+    53, // input 49 (0x31 char '1') => 53 (0x35)
+    54, // input 50 (0x32 char '2') => 54 (0x36)
+    55, // input 51 (0x33 char '3') => 55 (0x37)
+    56, // input 52 (0x34 char '4') => 56 (0x38)
+    57, // input 53 (0x35 char '5') => 57 (0x39)
+    58, // input 54 (0x36 char '6') => 58 (0x3A)
+    59, // input 55 (0x37 char '7') => 59 (0x3B)
+    60, // input 56 (0x38 char '8') => 60 (0x3C)
+    61, // input 57 (0x39 char '9') => 61 (0x3D)
+    INVALID_VALUE, // input 58 (0x3A)
+    INVALID_VALUE, // input 59 (0x3B)
+    INVALID_VALUE, // input 60 (0x3C)
+    INVALID_VALUE, // input 61 (0x3D)
+    INVALID_VALUE, // input 62 (0x3E)
+    INVALID_VALUE, // input 63 (0x3F)
+    INVALID_VALUE, // input 64 (0x40)
+    0, // input 65 (0x41 char 'A') => 0 (0x0)
+    1, // input 66 (0x42 char 'B') => 1 (0x1)
+    2, // input 67 (0x43 char 'C') => 2 (0x2)
+    3, // input 68 (0x44 char 'D') => 3 (0x3)
+    4, // input 69 (0x45 char 'E') => 4 (0x4)
+    5, // input 70 (0x46 char 'F') => 5 (0x5)
+    6, // input 71 (0x47 char 'G') => 6 (0x6)
+    7, // input 72 (0x48 char 'H') => 7 (0x7)
+    8, // input 73 (0x49 char 'I') => 8 (0x8)
+    9, // input 74 (0x4A char 'J') => 9 (0x9)
+    10, // input 75 (0x4B char 'K') => 10 (0xA)
+    11, // input 76 (0x4C char 'L') => 11 (0xB)
+    12, // input 77 (0x4D char 'M') => 12 (0xC)
+    13, // input 78 (0x4E char 'N') => 13 (0xD)
+    14, // input 79 (0x4F char 'O') => 14 (0xE)
+    15, // input 80 (0x50 char 'P') => 15 (0xF)
+    16, // input 81 (0x51 char 'Q') => 16 (0x10)
+    17, // input 82 (0x52 char 'R') => 17 (0x11)
+    18, // input 83 (0x53 char 'S') => 18 (0x12)
+    19, // input 84 (0x54 char 'T') => 19 (0x13)
+    20, // input 85 (0x55 char 'U') => 20 (0x14)
+    21, // input 86 (0x56 char 'V') => 21 (0x15)
+    22, // input 87 (0x57 char 'W') => 22 (0x16)
+    23, // input 88 (0x58 char 'X') => 23 (0x17)
+    24, // input 89 (0x59 char 'Y') => 24 (0x18)
+    25, // input 90 (0x5A char 'Z') => 25 (0x19)
+    INVALID_VALUE, // input 91 (0x5B)
+    INVALID_VALUE, // input 92 (0x5C)
+    INVALID_VALUE, // input 93 (0x5D)
+    INVALID_VALUE, // input 94 (0x5E)
+    INVALID_VALUE, // input 95 (0x5F)
+    INVALID_VALUE, // input 96 (0x60)
+    26, // input 97 (0x61 char 'a') => 26 (0x1A)
+    27, // input 98 (0x62 char 'b') => 27 (0x1B)
+    28, // input 99 (0x63 char 'c') => 28 (0x1C)
+    29, // input 100 (0x64 char 'd') => 29 (0x1D)
+    30, // input 101 (0x65 char 'e') => 30 (0x1E)
+    31, // input 102 (0x66 char 'f') => 31 (0x1F)
+    32, // input 103 (0x67 char 'g') => 32 (0x20)
+    33, // input 104 (0x68 char 'h') => 33 (0x21)
+    34, // input 105 (0x69 char 'i') => 34 (0x22)
+    35, // input 106 (0x6A char 'j') => 35 (0x23)
+    36, // input 107 (0x6B char 'k') => 36 (0x24)
+    37, // input 108 (0x6C char 'l') => 37 (0x25)
+    38, // input 109 (0x6D char 'm') => 38 (0x26)
+    39, // input 110 (0x6E char 'n') => 39 (0x27)
+    40, // input 111 (0x6F char 'o') => 40 (0x28)
+    41, // input 112 (0x70 char 'p') => 41 (0x29)
+    42, // input 113 (0x71 char 'q') => 42 (0x2A)
+    43, // input 114 (0x72 char 'r') => 43 (0x2B)
+    44, // input 115 (0x73 char 's') => 44 (0x2C)
+    45, // input 116 (0x74 char 't') => 45 (0x2D)
+    46, // input 117 (0x75 char 'u') => 46 (0x2E)
+    47, // input 118 (0x76 char 'v') => 47 (0x2F)
+    48, // input 119 (0x77 char 'w') => 48 (0x30)
+    49, // input 120 (0x78 char 'x') => 49 (0x31)
+    50, // input 121 (0x79 char 'y') => 50 (0x32)
+    51, // input 122 (0x7A char 'z') => 51 (0x33)
+    INVALID_VALUE, // input 123 (0x7B)
+    INVALID_VALUE, // input 124 (0x7C)
+    INVALID_VALUE, // input 125 (0x7D)
+    INVALID_VALUE, // input 126 (0x7E)
+    INVALID_VALUE, // input 127 (0x7F)
+    INVALID_VALUE, // input 128 (0x80)
+    INVALID_VALUE, // input 129 (0x81)
+    INVALID_VALUE, // input 130 (0x82)
+    INVALID_VALUE, // input 131 (0x83)
+    INVALID_VALUE, // input 132 (0x84)
+    INVALID_VALUE, // input 133 (0x85)
+    INVALID_VALUE, // input 134 (0x86)
+    INVALID_VALUE, // input 135 (0x87)
+    INVALID_VALUE, // input 136 (0x88)
+    INVALID_VALUE, // input 137 (0x89)
+    INVALID_VALUE, // input 138 (0x8A)
+    INVALID_VALUE, // input 139 (0x8B)
+    INVALID_VALUE, // input 140 (0x8C)
+    INVALID_VALUE, // input 141 (0x8D)
+    INVALID_VALUE, // input 142 (0x8E)
+    INVALID_VALUE, // input 143 (0x8F)
+    INVALID_VALUE, // input 144 (0x90)
+    INVALID_VALUE, // input 145 (0x91)
+    INVALID_VALUE, // input 146 (0x92)
+    INVALID_VALUE, // input 147 (0x93)
+    INVALID_VALUE, // input 148 (0x94)
+    INVALID_VALUE, // input 149 (0x95)
+    INVALID_VALUE, // input 150 (0x96)
+    INVALID_VALUE, // input 151 (0x97)
+    INVALID_VALUE, // input 152 (0x98)
+    INVALID_VALUE, // input 153 (0x99)
+    INVALID_VALUE, // input 154 (0x9A)
+    INVALID_VALUE, // input 155 (0x9B)
+    INVALID_VALUE, // input 156 (0x9C)
+    INVALID_VALUE, // input 157 (0x9D)
+    INVALID_VALUE, // input 158 (0x9E)
+    INVALID_VALUE, // input 159 (0x9F)
+    INVALID_VALUE, // input 160 (0xA0)
+    INVALID_VALUE, // input 161 (0xA1)
+    INVALID_VALUE, // input 162 (0xA2)
+    INVALID_VALUE, // input 163 (0xA3)
+    INVALID_VALUE, // input 164 (0xA4)
+    INVALID_VALUE, // input 165 (0xA5)
+    INVALID_VALUE, // input 166 (0xA6)
+    INVALID_VALUE, // input 167 (0xA7)
+    INVALID_VALUE, // input 168 (0xA8)
+    INVALID_VALUE, // input 169 (0xA9)
+    INVALID_VALUE, // input 170 (0xAA)
+    INVALID_VALUE, // input 171 (0xAB)
+    INVALID_VALUE, // input 172 (0xAC)
+    INVALID_VALUE, // input 173 (0xAD)
+    INVALID_VALUE, // input 174 (0xAE)
+    INVALID_VALUE, // input 175 (0xAF)
+    INVALID_VALUE, // input 176 (0xB0)
+    INVALID_VALUE, // input 177 (0xB1)
+    INVALID_VALUE, // input 178 (0xB2)
+    INVALID_VALUE, // input 179 (0xB3)
+    INVALID_VALUE, // input 180 (0xB4)
+    INVALID_VALUE, // input 181 (0xB5)
+    INVALID_VALUE, // input 182 (0xB6)
+    INVALID_VALUE, // input 183 (0xB7)
+    INVALID_VALUE, // input 184 (0xB8)
+    INVALID_VALUE, // input 185 (0xB9)
+    INVALID_VALUE, // input 186 (0xBA)
+    INVALID_VALUE, // input 187 (0xBB)
+    INVALID_VALUE, // input 188 (0xBC)
+    INVALID_VALUE, // input 189 (0xBD)
+    INVALID_VALUE, // input 190 (0xBE)
+    INVALID_VALUE, // input 191 (0xBF)
+    INVALID_VALUE, // input 192 (0xC0)
+    INVALID_VALUE, // input 193 (0xC1)
+    INVALID_VALUE, // input 194 (0xC2)
+    INVALID_VALUE, // input 195 (0xC3)
+    INVALID_VALUE, // input 196 (0xC4)
+    INVALID_VALUE, // input 197 (0xC5)
+    INVALID_VALUE, // input 198 (0xC6)
+    INVALID_VALUE, // input 199 (0xC7)
+    INVALID_VALUE, // input 200 (0xC8)
+    INVALID_VALUE, // input 201 (0xC9)
+    INVALID_VALUE, // input 202 (0xCA)
+    INVALID_VALUE, // input 203 (0xCB)
+    INVALID_VALUE, // input 204 (0xCC)
+    INVALID_VALUE, // input 205 (0xCD)
+    INVALID_VALUE, // input 206 (0xCE)
+    INVALID_VALUE, // input 207 (0xCF)
+    INVALID_VALUE, // input 208 (0xD0)
+    INVALID_VALUE, // input 209 (0xD1)
+    INVALID_VALUE, // input 210 (0xD2)
+    INVALID_VALUE, // input 211 (0xD3)
+    INVALID_VALUE, // input 212 (0xD4)
+    INVALID_VALUE, // input 213 (0xD5)
+    INVALID_VALUE, // input 214 (0xD6)
+    INVALID_VALUE, // input 215 (0xD7)
+    INVALID_VALUE, // input 216 (0xD8)
+    INVALID_VALUE, // input 217 (0xD9)
+    INVALID_VALUE, // input 218 (0xDA)
+    INVALID_VALUE, // input 219 (0xDB)
+    INVALID_VALUE, // input 220 (0xDC)
+    INVALID_VALUE, // input 221 (0xDD)
+    INVALID_VALUE, // input 222 (0xDE)
+    INVALID_VALUE, // input 223 (0xDF)
+    INVALID_VALUE, // input 224 (0xE0)
+    INVALID_VALUE, // input 225 (0xE1)
+    INVALID_VALUE, // input 226 (0xE2)
+    INVALID_VALUE, // input 227 (0xE3)
+    INVALID_VALUE, // input 228 (0xE4)
+    INVALID_VALUE, // input 229 (0xE5)
+    INVALID_VALUE, // input 230 (0xE6)
+    INVALID_VALUE, // input 231 (0xE7)
+    INVALID_VALUE, // input 232 (0xE8)
+    INVALID_VALUE, // input 233 (0xE9)
+    INVALID_VALUE, // input 234 (0xEA)
+    INVALID_VALUE, // input 235 (0xEB)
+    INVALID_VALUE, // input 236 (0xEC)
+    INVALID_VALUE, // input 237 (0xED)
+    INVALID_VALUE, // input 238 (0xEE)
+    INVALID_VALUE, // input 239 (0xEF)
+    INVALID_VALUE, // input 240 (0xF0)
+    INVALID_VALUE, // input 241 (0xF1)
+    INVALID_VALUE, // input 242 (0xF2)
+    INVALID_VALUE, // input 243 (0xF3)
+    INVALID_VALUE, // input 244 (0xF4)
+    INVALID_VALUE, // input 245 (0xF5)
+    INVALID_VALUE, // input 246 (0xF6)
+    INVALID_VALUE, // input 247 (0xF7)
+    INVALID_VALUE, // input 248 (0xF8)
+    INVALID_VALUE, // input 249 (0xF9)
+    INVALID_VALUE, // input 250 (0xFA)
+    INVALID_VALUE, // input 251 (0xFB)
+    INVALID_VALUE, // input 252 (0xFC)
+    INVALID_VALUE, // input 253 (0xFD)
+    INVALID_VALUE, // input 254 (0xFE)
+    INVALID_VALUE, // input 255 (0xFF)
+];
+#[rustfmt::skip]
+pub const URL_SAFE_ENCODE: &[u8; 64] = &[
+    65, // input 0 (0x0) => 'A' (0x41)
+    66, // input 1 (0x1) => 'B' (0x42)
+    67, // input 2 (0x2) => 'C' (0x43)
+    68, // input 3 (0x3) => 'D' (0x44)
+    69, // input 4 (0x4) => 'E' (0x45)
+    70, // input 5 (0x5) => 'F' (0x46)
+    71, // input 6 (0x6) => 'G' (0x47)
+    72, // input 7 (0x7) => 'H' (0x48)
+    73, // input 8 (0x8) => 'I' (0x49)
+    74, // input 9 (0x9) => 'J' (0x4A)
+    75, // input 10 (0xA) => 'K' (0x4B)
+    76, // input 11 (0xB) => 'L' (0x4C)
+    77, // input 12 (0xC) => 'M' (0x4D)
+    78, // input 13 (0xD) => 'N' (0x4E)
+    79, // input 14 (0xE) => 'O' (0x4F)
+    80, // input 15 (0xF) => 'P' (0x50)
+    81, // input 16 (0x10) => 'Q' (0x51)
+    82, // input 17 (0x11) => 'R' (0x52)
+    83, // input 18 (0x12) => 'S' (0x53)
+    84, // input 19 (0x13) => 'T' (0x54)
+    85, // input 20 (0x14) => 'U' (0x55)
+    86, // input 21 (0x15) => 'V' (0x56)
+    87, // input 22 (0x16) => 'W' (0x57)
+    88, // input 23 (0x17) => 'X' (0x58)
+    89, // input 24 (0x18) => 'Y' (0x59)
+    90, // input 25 (0x19) => 'Z' (0x5A)
+    97, // input 26 (0x1A) => 'a' (0x61)
+    98, // input 27 (0x1B) => 'b' (0x62)
+    99, // input 28 (0x1C) => 'c' (0x63)
+    100, // input 29 (0x1D) => 'd' (0x64)
+    101, // input 30 (0x1E) => 'e' (0x65)
+    102, // input 31 (0x1F) => 'f' (0x66)
+    103, // input 32 (0x20) => 'g' (0x67)
+    104, // input 33 (0x21) => 'h' (0x68)
+    105, // input 34 (0x22) => 'i' (0x69)
+    106, // input 35 (0x23) => 'j' (0x6A)
+    107, // input 36 (0x24) => 'k' (0x6B)
+    108, // input 37 (0x25) => 'l' (0x6C)
+    109, // input 38 (0x26) => 'm' (0x6D)
+    110, // input 39 (0x27) => 'n' (0x6E)
+    111, // input 40 (0x28) => 'o' (0x6F)
+    112, // input 41 (0x29) => 'p' (0x70)
+    113, // input 42 (0x2A) => 'q' (0x71)
+    114, // input 43 (0x2B) => 'r' (0x72)
+    115, // input 44 (0x2C) => 's' (0x73)
+    116, // input 45 (0x2D) => 't' (0x74)
+    117, // input 46 (0x2E) => 'u' (0x75)
+    118, // input 47 (0x2F) => 'v' (0x76)
+    119, // input 48 (0x30) => 'w' (0x77)
+    120, // input 49 (0x31) => 'x' (0x78)
+    121, // input 50 (0x32) => 'y' (0x79)
+    122, // input 51 (0x33) => 'z' (0x7A)
+    48, // input 52 (0x34) => '0' (0x30)
+    49, // input 53 (0x35) => '1' (0x31)
+    50, // input 54 (0x36) => '2' (0x32)
+    51, // input 55 (0x37) => '3' (0x33)
+    52, // input 56 (0x38) => '4' (0x34)
+    53, // input 57 (0x39) => '5' (0x35)
+    54, // input 58 (0x3A) => '6' (0x36)
+    55, // input 59 (0x3B) => '7' (0x37)
+    56, // input 60 (0x3C) => '8' (0x38)
+    57, // input 61 (0x3D) => '9' (0x39)
+    45, // input 62 (0x3E) => '-' (0x2D)
+    95, // input 63 (0x3F) => '_' (0x5F)
+];
+#[rustfmt::skip]
+pub const URL_SAFE_DECODE: &[u8; 256] = &[
+    INVALID_VALUE, // input 0 (0x0)
+    INVALID_VALUE, // input 1 (0x1)
+    INVALID_VALUE, // input 2 (0x2)
+    INVALID_VALUE, // input 3 (0x3)
+    INVALID_VALUE, // input 4 (0x4)
+    INVALID_VALUE, // input 5 (0x5)
+    INVALID_VALUE, // input 6 (0x6)
+    INVALID_VALUE, // input 7 (0x7)
+    INVALID_VALUE, // input 8 (0x8)
+    INVALID_VALUE, // input 9 (0x9)
+    INVALID_VALUE, // input 10 (0xA)
+    INVALID_VALUE, // input 11 (0xB)
+    INVALID_VALUE, // input 12 (0xC)
+    INVALID_VALUE, // input 13 (0xD)
+    INVALID_VALUE, // input 14 (0xE)
+    INVALID_VALUE, // input 15 (0xF)
+    INVALID_VALUE, // input 16 (0x10)
+    INVALID_VALUE, // input 17 (0x11)
+    INVALID_VALUE, // input 18 (0x12)
+    INVALID_VALUE, // input 19 (0x13)
+    INVALID_VALUE, // input 20 (0x14)
+    INVALID_VALUE, // input 21 (0x15)
+    INVALID_VALUE, // input 22 (0x16)
+    INVALID_VALUE, // input 23 (0x17)
+    INVALID_VALUE, // input 24 (0x18)
+    INVALID_VALUE, // input 25 (0x19)
+    INVALID_VALUE, // input 26 (0x1A)
+    INVALID_VALUE, // input 27 (0x1B)
+    INVALID_VALUE, // input 28 (0x1C)
+    INVALID_VALUE, // input 29 (0x1D)
+    INVALID_VALUE, // input 30 (0x1E)
+    INVALID_VALUE, // input 31 (0x1F)
+    INVALID_VALUE, // input 32 (0x20)
+    INVALID_VALUE, // input 33 (0x21)
+    INVALID_VALUE, // input 34 (0x22)
+    INVALID_VALUE, // input 35 (0x23)
+    INVALID_VALUE, // input 36 (0x24)
+    INVALID_VALUE, // input 37 (0x25)
+    INVALID_VALUE, // input 38 (0x26)
+    INVALID_VALUE, // input 39 (0x27)
+    INVALID_VALUE, // input 40 (0x28)
+    INVALID_VALUE, // input 41 (0x29)
+    INVALID_VALUE, // input 42 (0x2A)
+    INVALID_VALUE, // input 43 (0x2B)
+    INVALID_VALUE, // input 44 (0x2C)
+    62, // input 45 (0x2D char '-') => 62 (0x3E)
+    INVALID_VALUE, // input 46 (0x2E)
+    INVALID_VALUE, // input 47 (0x2F)
+    52, // input 48 (0x30 char '0') => 52 (0x34)
+    53, // input 49 (0x31 char '1') => 53 (0x35)
+    54, // input 50 (0x32 char '2') => 54 (0x36)
+    55, // input 51 (0x33 char '3') => 55 (0x37)
+    56, // input 52 (0x34 char '4') => 56 (0x38)
+    57, // input 53 (0x35 char '5') => 57 (0x39)
+    58, // input 54 (0x36 char '6') => 58 (0x3A)
+    59, // input 55 (0x37 char '7') => 59 (0x3B)
+    60, // input 56 (0x38 char '8') => 60 (0x3C)
+    61, // input 57 (0x39 char '9') => 61 (0x3D)
+    INVALID_VALUE, // input 58 (0x3A)
+    INVALID_VALUE, // input 59 (0x3B)
+    INVALID_VALUE, // input 60 (0x3C)
+    INVALID_VALUE, // input 61 (0x3D)
+    INVALID_VALUE, // input 62 (0x3E)
+    INVALID_VALUE, // input 63 (0x3F)
+    INVALID_VALUE, // input 64 (0x40)
+    0, // input 65 (0x41 char 'A') => 0 (0x0)
+    1, // input 66 (0x42 char 'B') => 1 (0x1)
+    2, // input 67 (0x43 char 'C') => 2 (0x2)
+    3, // input 68 (0x44 char 'D') => 3 (0x3)
+    4, // input 69 (0x45 char 'E') => 4 (0x4)
+    5, // input 70 (0x46 char 'F') => 5 (0x5)
+    6, // input 71 (0x47 char 'G') => 6 (0x6)
+    7, // input 72 (0x48 char 'H') => 7 (0x7)
+    8, // input 73 (0x49 char 'I') => 8 (0x8)
+    9, // input 74 (0x4A char 'J') => 9 (0x9)
+    10, // input 75 (0x4B char 'K') => 10 (0xA)
+    11, // input 76 (0x4C char 'L') => 11 (0xB)
+    12, // input 77 (0x4D char 'M') => 12 (0xC)
+    13, // input 78 (0x4E char 'N') => 13 (0xD)
+    14, // input 79 (0x4F char 'O') => 14 (0xE)
+    15, // input 80 (0x50 char 'P') => 15 (0xF)
+    16, // input 81 (0x51 char 'Q') => 16 (0x10)
+    17, // input 82 (0x52 char 'R') => 17 (0x11)
+    18, // input 83 (0x53 char 'S') => 18 (0x12)
+    19, // input 84 (0x54 char 'T') => 19 (0x13)
+    20, // input 85 (0x55 char 'U') => 20 (0x14)
+    21, // input 86 (0x56 char 'V') => 21 (0x15)
+    22, // input 87 (0x57 char 'W') => 22 (0x16)
+    23, // input 88 (0x58 char 'X') => 23 (0x17)
+    24, // input 89 (0x59 char 'Y') => 24 (0x18)
+    25, // input 90 (0x5A char 'Z') => 25 (0x19)
+    INVALID_VALUE, // input 91 (0x5B)
+    INVALID_VALUE, // input 92 (0x5C)
+    INVALID_VALUE, // input 93 (0x5D)
+    INVALID_VALUE, // input 94 (0x5E)
+    63, // input 95 (0x5F char '_') => 63 (0x3F)
+    INVALID_VALUE, // input 96 (0x60)
+    26, // input 97 (0x61 char 'a') => 26 (0x1A)
+    27, // input 98 (0x62 char 'b') => 27 (0x1B)
+    28, // input 99 (0x63 char 'c') => 28 (0x1C)
+    29, // input 100 (0x64 char 'd') => 29 (0x1D)
+    30, // input 101 (0x65 char 'e') => 30 (0x1E)
+    31, // input 102 (0x66 char 'f') => 31 (0x1F)
+    32, // input 103 (0x67 char 'g') => 32 (0x20)
+    33, // input 104 (0x68 char 'h') => 33 (0x21)
+    34, // input 105 (0x69 char 'i') => 34 (0x22)
+    35, // input 106 (0x6A char 'j') => 35 (0x23)
+    36, // input 107 (0x6B char 'k') => 36 (0x24)
+    37, // input 108 (0x6C char 'l') => 37 (0x25)
+    38, // input 109 (0x6D char 'm') => 38 (0x26)
+    39, // input 110 (0x6E char 'n') => 39 (0x27)
+    40, // input 111 (0x6F char 'o') => 40 (0x28)
+    41, // input 112 (0x70 char 'p') => 41 (0x29)
+    42, // input 113 (0x71 char 'q') => 42 (0x2A)
+    43, // input 114 (0x72 char 'r') => 43 (0x2B)
+    44, // input 115 (0x73 char 's') => 44 (0x2C)
+    45, // input 116 (0x74 char 't') => 45 (0x2D)
+    46, // input 117 (0x75 char 'u') => 46 (0x2E)
+    47, // input 118 (0x76 char 'v') => 47 (0x2F)
+    48, // input 119 (0x77 char 'w') => 48 (0x30)
+    49, // input 120 (0x78 char 'x') => 49 (0x31)
+    50, // input 121 (0x79 char 'y') => 50 (0x32)
+    51, // input 122 (0x7A char 'z') => 51 (0x33)
+    INVALID_VALUE, // input 123 (0x7B)
+    INVALID_VALUE, // input 124 (0x7C)
+    INVALID_VALUE, // input 125 (0x7D)
+    INVALID_VALUE, // input 126 (0x7E)
+    INVALID_VALUE, // input 127 (0x7F)
+    INVALID_VALUE, // input 128 (0x80)
+    INVALID_VALUE, // input 129 (0x81)
+    INVALID_VALUE, // input 130 (0x82)
+    INVALID_VALUE, // input 131 (0x83)
+    INVALID_VALUE, // input 132 (0x84)
+    INVALID_VALUE, // input 133 (0x85)
+    INVALID_VALUE, // input 134 (0x86)
+    INVALID_VALUE, // input 135 (0x87)
+    INVALID_VALUE, // input 136 (0x88)
+    INVALID_VALUE, // input 137 (0x89)
+    INVALID_VALUE, // input 138 (0x8A)
+    INVALID_VALUE, // input 139 (0x8B)
+    INVALID_VALUE, // input 140 (0x8C)
+    INVALID_VALUE, // input 141 (0x8D)
+    INVALID_VALUE, // input 142 (0x8E)
+    INVALID_VALUE, // input 143 (0x8F)
+    INVALID_VALUE, // input 144 (0x90)
+    INVALID_VALUE, // input 145 (0x91)
+    INVALID_VALUE, // input 146 (0x92)
+    INVALID_VALUE, // input 147 (0x93)
+    INVALID_VALUE, // input 148 (0x94)
+    INVALID_VALUE, // input 149 (0x95)
+    INVALID_VALUE, // input 150 (0x96)
+    INVALID_VALUE, // input 151 (0x97)
+    INVALID_VALUE, // input 152 (0x98)
+    INVALID_VALUE, // input 153 (0x99)
+    INVALID_VALUE, // input 154 (0x9A)
+    INVALID_VALUE, // input 155 (0x9B)
+    INVALID_VALUE, // input 156 (0x9C)
+    INVALID_VALUE, // input 157 (0x9D)
+    INVALID_VALUE, // input 158 (0x9E)
+    INVALID_VALUE, // input 159 (0x9F)
+    INVALID_VALUE, // input 160 (0xA0)
+    INVALID_VALUE, // input 161 (0xA1)
+    INVALID_VALUE, // input 162 (0xA2)
+    INVALID_VALUE, // input 163 (0xA3)
+    INVALID_VALUE, // input 164 (0xA4)
+    INVALID_VALUE, // input 165 (0xA5)
+    INVALID_VALUE, // input 166 (0xA6)
+    INVALID_VALUE, // input 167 (0xA7)
+    INVALID_VALUE, // input 168 (0xA8)
+    INVALID_VALUE, // input 169 (0xA9)
+    INVALID_VALUE, // input 170 (0xAA)
+    INVALID_VALUE, // input 171 (0xAB)
+    INVALID_VALUE, // input 172 (0xAC)
+    INVALID_VALUE, // input 173 (0xAD)
+    INVALID_VALUE, // input 174 (0xAE)
+    INVALID_VALUE, // input 175 (0xAF)
+    INVALID_VALUE, // input 176 (0xB0)
+    INVALID_VALUE, // input 177 (0xB1)
+    INVALID_VALUE, // input 178 (0xB2)
+    INVALID_VALUE, // input 179 (0xB3)
+    INVALID_VALUE, // input 180 (0xB4)
+    INVALID_VALUE, // input 181 (0xB5)
+    INVALID_VALUE, // input 182 (0xB6)
+    INVALID_VALUE, // input 183 (0xB7)
+    INVALID_VALUE, // input 184 (0xB8)
+    INVALID_VALUE, // input 185 (0xB9)
+    INVALID_VALUE, // input 186 (0xBA)
+    INVALID_VALUE, // input 187 (0xBB)
+    INVALID_VALUE, // input 188 (0xBC)
+    INVALID_VALUE, // input 189 (0xBD)
+    INVALID_VALUE, // input 190 (0xBE)
+    INVALID_VALUE, // input 191 (0xBF)
+    INVALID_VALUE, // input 192 (0xC0)
+    INVALID_VALUE, // input 193 (0xC1)
+    INVALID_VALUE, // input 194 (0xC2)
+    INVALID_VALUE, // input 195 (0xC3)
+    INVALID_VALUE, // input 196 (0xC4)
+    INVALID_VALUE, // input 197 (0xC5)
+    INVALID_VALUE, // input 198 (0xC6)
+    INVALID_VALUE, // input 199 (0xC7)
+    INVALID_VALUE, // input 200 (0xC8)
+    INVALID_VALUE, // input 201 (0xC9)
+    INVALID_VALUE, // input 202 (0xCA)
+    INVALID_VALUE, // input 203 (0xCB)
+    INVALID_VALUE, // input 204 (0xCC)
+    INVALID_VALUE, // input 205 (0xCD)
+    INVALID_VALUE, // input 206 (0xCE)
+    INVALID_VALUE, // input 207 (0xCF)
+    INVALID_VALUE, // input 208 (0xD0)
+    INVALID_VALUE, // input 209 (0xD1)
+    INVALID_VALUE, // input 210 (0xD2)
+    INVALID_VALUE, // input 211 (0xD3)
+    INVALID_VALUE, // input 212 (0xD4)
+    INVALID_VALUE, // input 213 (0xD5)
+    INVALID_VALUE, // input 214 (0xD6)
+    INVALID_VALUE, // input 215 (0xD7)
+    INVALID_VALUE, // input 216 (0xD8)
+    INVALID_VALUE, // input 217 (0xD9)
+    INVALID_VALUE, // input 218 (0xDA)
+    INVALID_VALUE, // input 219 (0xDB)
+    INVALID_VALUE, // input 220 (0xDC)
+    INVALID_VALUE, // input 221 (0xDD)
+    INVALID_VALUE, // input 222 (0xDE)
+    INVALID_VALUE, // input 223 (0xDF)
+    INVALID_VALUE, // input 224 (0xE0)
+    INVALID_VALUE, // input 225 (0xE1)
+    INVALID_VALUE, // input 226 (0xE2)
+    INVALID_VALUE, // input 227 (0xE3)
+    INVALID_VALUE, // input 228 (0xE4)
+    INVALID_VALUE, // input 229 (0xE5)
+    INVALID_VALUE, // input 230 (0xE6)
+    INVALID_VALUE, // input 231 (0xE7)
+    INVALID_VALUE, // input 232 (0xE8)
+    INVALID_VALUE, // input 233 (0xE9)
+    INVALID_VALUE, // input 234 (0xEA)
+    INVALID_VALUE, // input 235 (0xEB)
+    INVALID_VALUE, // input 236 (0xEC)
+    INVALID_VALUE, // input 237 (0xED)
+    INVALID_VALUE, // input 238 (0xEE)
+    INVALID_VALUE, // input 239 (0xEF)
+    INVALID_VALUE, // input 240 (0xF0)
+    INVALID_VALUE, // input 241 (0xF1)
+    INVALID_VALUE, // input 242 (0xF2)
+    INVALID_VALUE, // input 243 (0xF3)
+    INVALID_VALUE, // input 244 (0xF4)
+    INVALID_VALUE, // input 245 (0xF5)
+    INVALID_VALUE, // input 246 (0xF6)
+    INVALID_VALUE, // input 247 (0xF7)
+    INVALID_VALUE, // input 248 (0xF8)
+    INVALID_VALUE, // input 249 (0xF9)
+    INVALID_VALUE, // input 250 (0xFA)
+    INVALID_VALUE, // input 251 (0xFB)
+    INVALID_VALUE, // input 252 (0xFC)
+    INVALID_VALUE, // input 253 (0xFD)
+    INVALID_VALUE, // input 254 (0xFE)
+    INVALID_VALUE, // input 255 (0xFF)
+];
+#[rustfmt::skip]
+pub const CRYPT_ENCODE: &[u8; 64] = &[
+    46, // input 0 (0x0) => '.' (0x2E)
+    47, // input 1 (0x1) => '/' (0x2F)
+    48, // input 2 (0x2) => '0' (0x30)
+    49, // input 3 (0x3) => '1' (0x31)
+    50, // input 4 (0x4) => '2' (0x32)
+    51, // input 5 (0x5) => '3' (0x33)
+    52, // input 6 (0x6) => '4' (0x34)
+    53, // input 7 (0x7) => '5' (0x35)
+    54, // input 8 (0x8) => '6' (0x36)
+    55, // input 9 (0x9) => '7' (0x37)
+    56, // input 10 (0xA) => '8' (0x38)
+    57, // input 11 (0xB) => '9' (0x39)
+    65, // input 12 (0xC) => 'A' (0x41)
+    66, // input 13 (0xD) => 'B' (0x42)
+    67, // input 14 (0xE) => 'C' (0x43)
+    68, // input 15 (0xF) => 'D' (0x44)
+    69, // input 16 (0x10) => 'E' (0x45)
+    70, // input 17 (0x11) => 'F' (0x46)
+    71, // input 18 (0x12) => 'G' (0x47)
+    72, // input 19 (0x13) => 'H' (0x48)
+    73, // input 20 (0x14) => 'I' (0x49)
+    74, // input 21 (0x15) => 'J' (0x4A)
+    75, // input 22 (0x16) => 'K' (0x4B)
+    76, // input 23 (0x17) => 'L' (0x4C)
+    77, // input 24 (0x18) => 'M' (0x4D)
+    78, // input 25 (0x19) => 'N' (0x4E)
+    79, // input 26 (0x1A) => 'O' (0x4F)
+    80, // input 27 (0x1B) => 'P' (0x50)
+    81, // input 28 (0x1C) => 'Q' (0x51)
+    82, // input 29 (0x1D) => 'R' (0x52)
+    83, // input 30 (0x1E) => 'S' (0x53)
+    84, // input 31 (0x1F) => 'T' (0x54)
+    85, // input 32 (0x20) => 'U' (0x55)
+    86, // input 33 (0x21) => 'V' (0x56)
+    87, // input 34 (0x22) => 'W' (0x57)
+    88, // input 35 (0x23) => 'X' (0x58)
+    89, // input 36 (0x24) => 'Y' (0x59)
+    90, // input 37 (0x25) => 'Z' (0x5A)
+    97, // input 38 (0x26) => 'a' (0x61)
+    98, // input 39 (0x27) => 'b' (0x62)
+    99, // input 40 (0x28) => 'c' (0x63)
+    100, // input 41 (0x29) => 'd' (0x64)
+    101, // input 42 (0x2A) => 'e' (0x65)
+    102, // input 43 (0x2B) => 'f' (0x66)
+    103, // input 44 (0x2C) => 'g' (0x67)
+    104, // input 45 (0x2D) => 'h' (0x68)
+    105, // input 46 (0x2E) => 'i' (0x69)
+    106, // input 47 (0x2F) => 'j' (0x6A)
+    107, // input 48 (0x30) => 'k' (0x6B)
+    108, // input 49 (0x31) => 'l' (0x6C)
+    109, // input 50 (0x32) => 'm' (0x6D)
+    110, // input 51 (0x33) => 'n' (0x6E)
+    111, // input 52 (0x34) => 'o' (0x6F)
+    112, // input 53 (0x35) => 'p' (0x70)
+    113, // input 54 (0x36) => 'q' (0x71)
+    114, // input 55 (0x37) => 'r' (0x72)
+    115, // input 56 (0x38) => 's' (0x73)
+    116, // input 57 (0x39) => 't' (0x74)
+    117, // input 58 (0x3A) => 'u' (0x75)
+    118, // input 59 (0x3B) => 'v' (0x76)
+    119, // input 60 (0x3C) => 'w' (0x77)
+    120, // input 61 (0x3D) => 'x' (0x78)
+    121, // input 62 (0x3E) => 'y' (0x79)
+    122, // input 63 (0x3F) => 'z' (0x7A)
+];
+#[rustfmt::skip]
+pub const CRYPT_DECODE: &[u8; 256] = &[
+    INVALID_VALUE, // input 0 (0x0)
+    INVALID_VALUE, // input 1 (0x1)
+    INVALID_VALUE, // input 2 (0x2)
+    INVALID_VALUE, // input 3 (0x3)
+    INVALID_VALUE, // input 4 (0x4)
+    INVALID_VALUE, // input 5 (0x5)
+    INVALID_VALUE, // input 6 (0x6)
+    INVALID_VALUE, // input 7 (0x7)
+    INVALID_VALUE, // input 8 (0x8)
+    INVALID_VALUE, // input 9 (0x9)
+    INVALID_VALUE, // input 10 (0xA)
+    INVALID_VALUE, // input 11 (0xB)
+    INVALID_VALUE, // input 12 (0xC)
+    INVALID_VALUE, // input 13 (0xD)
+    INVALID_VALUE, // input 14 (0xE)
+    INVALID_VALUE, // input 15 (0xF)
+    INVALID_VALUE, // input 16 (0x10)
+    INVALID_VALUE, // input 17 (0x11)
+    INVALID_VALUE, // input 18 (0x12)
+    INVALID_VALUE, // input 19 (0x13)
+    INVALID_VALUE, // input 20 (0x14)
+    INVALID_VALUE, // input 21 (0x15)
+    INVALID_VALUE, // input 22 (0x16)
+    INVALID_VALUE, // input 23 (0x17)
+    INVALID_VALUE, // input 24 (0x18)
+    INVALID_VALUE, // input 25 (0x19)
+    INVALID_VALUE, // input 26 (0x1A)
+    INVALID_VALUE, // input 27 (0x1B)
+    INVALID_VALUE, // input 28 (0x1C)
+    INVALID_VALUE, // input 29 (0x1D)
+    INVALID_VALUE, // input 30 (0x1E)
+    INVALID_VALUE, // input 31 (0x1F)
+    INVALID_VALUE, // input 32 (0x20)
+    INVALID_VALUE, // input 33 (0x21)
+    INVALID_VALUE, // input 34 (0x22)
+    INVALID_VALUE, // input 35 (0x23)
+    INVALID_VALUE, // input 36 (0x24)
+    INVALID_VALUE, // input 37 (0x25)
+    INVALID_VALUE, // input 38 (0x26)
+    INVALID_VALUE, // input 39 (0x27)
+    INVALID_VALUE, // input 40 (0x28)
+    INVALID_VALUE, // input 41 (0x29)
+    INVALID_VALUE, // input 42 (0x2A)
+    INVALID_VALUE, // input 43 (0x2B)
+    INVALID_VALUE, // input 44 (0x2C)
+    INVALID_VALUE, // input 45 (0x2D)
+    0, // input 46 (0x2E char '.') => 0 (0x0)
+    1, // input 47 (0x2F char '/') => 1 (0x1)
+    2, // input 48 (0x30 char '0') => 2 (0x2)
+    3, // input 49 (0x31 char '1') => 3 (0x3)
+    4, // input 50 (0x32 char '2') => 4 (0x4)
+    5, // input 51 (0x33 char '3') => 5 (0x5)
+    6, // input 52 (0x34 char '4') => 6 (0x6)
+    7, // input 53 (0x35 char '5') => 7 (0x7)
+    8, // input 54 (0x36 char '6') => 8 (0x8)
+    9, // input 55 (0x37 char '7') => 9 (0x9)
+    10, // input 56 (0x38 char '8') => 10 (0xA)
+    11, // input 57 (0x39 char '9') => 11 (0xB)
+    INVALID_VALUE, // input 58 (0x3A)
+    INVALID_VALUE, // input 59 (0x3B)
+    INVALID_VALUE, // input 60 (0x3C)
+    INVALID_VALUE, // input 61 (0x3D)
+    INVALID_VALUE, // input 62 (0x3E)
+    INVALID_VALUE, // input 63 (0x3F)
+    INVALID_VALUE, // input 64 (0x40)
+    12, // input 65 (0x41 char 'A') => 12 (0xC)
+    13, // input 66 (0x42 char 'B') => 13 (0xD)
+    14, // input 67 (0x43 char 'C') => 14 (0xE)
+    15, // input 68 (0x44 char 'D') => 15 (0xF)
+    16, // input 69 (0x45 char 'E') => 16 (0x10)
+    17, // input 70 (0x46 char 'F') => 17 (0x11)
+    18, // input 71 (0x47 char 'G') => 18 (0x12)
+    19, // input 72 (0x48 char 'H') => 19 (0x13)
+    20, // input 73 (0x49 char 'I') => 20 (0x14)
+    21, // input 74 (0x4A char 'J') => 21 (0x15)
+    22, // input 75 (0x4B char 'K') => 22 (0x16)
+    23, // input 76 (0x4C char 'L') => 23 (0x17)
+    24, // input 77 (0x4D char 'M') => 24 (0x18)
+    25, // input 78 (0x4E char 'N') => 25 (0x19)
+    26, // input 79 (0x4F char 'O') => 26 (0x1A)
+    27, // input 80 (0x50 char 'P') => 27 (0x1B)
+    28, // input 81 (0x51 char 'Q') => 28 (0x1C)
+    29, // input 82 (0x52 char 'R') => 29 (0x1D)
+    30, // input 83 (0x53 char 'S') => 30 (0x1E)
+    31, // input 84 (0x54 char 'T') => 31 (0x1F)
+    32, // input 85 (0x55 char 'U') => 32 (0x20)
+    33, // input 86 (0x56 char 'V') => 33 (0x21)
+    34, // input 87 (0x57 char 'W') => 34 (0x22)
+    35, // input 88 (0x58 char 'X') => 35 (0x23)
+    36, // input 89 (0x59 char 'Y') => 36 (0x24)
+    37, // input 90 (0x5A char 'Z') => 37 (0x25)
+    INVALID_VALUE, // input 91 (0x5B)
+    INVALID_VALUE, // input 92 (0x5C)
+    INVALID_VALUE, // input 93 (0x5D)
+    INVALID_VALUE, // input 94 (0x5E)
+    INVALID_VALUE, // input 95 (0x5F)
+    INVALID_VALUE, // input 96 (0x60)
+    38, // input 97 (0x61 char 'a') => 38 (0x26)
+    39, // input 98 (0x62 char 'b') => 39 (0x27)
+    40, // input 99 (0x63 char 'c') => 40 (0x28)
+    41, // input 100 (0x64 char 'd') => 41 (0x29)
+    42, // input 101 (0x65 char 'e') => 42 (0x2A)
+    43, // input 102 (0x66 char 'f') => 43 (0x2B)
+    44, // input 103 (0x67 char 'g') => 44 (0x2C)
+    45, // input 104 (0x68 char 'h') => 45 (0x2D)
+    46, // input 105 (0x69 char 'i') => 46 (0x2E)
+    47, // input 106 (0x6A char 'j') => 47 (0x2F)
+    48, // input 107 (0x6B char 'k') => 48 (0x30)
+    49, // input 108 (0x6C char 'l') => 49 (0x31)
+    50, // input 109 (0x6D char 'm') => 50 (0x32)
+    51, // input 110 (0x6E char 'n') => 51 (0x33)
+    52, // input 111 (0x6F char 'o') => 52 (0x34)
+    53, // input 112 (0x70 char 'p') => 53 (0x35)
+    54, // input 113 (0x71 char 'q') => 54 (0x36)
+    55, // input 114 (0x72 char 'r') => 55 (0x37)
+    56, // input 115 (0x73 char 's') => 56 (0x38)
+    57, // input 116 (0x74 char 't') => 57 (0x39)
+    58, // input 117 (0x75 char 'u') => 58 (0x3A)
+    59, // input 118 (0x76 char 'v') => 59 (0x3B)
+    60, // input 119 (0x77 char 'w') => 60 (0x3C)
+    61, // input 120 (0x78 char 'x') => 61 (0x3D)
+    62, // input 121 (0x79 char 'y') => 62 (0x3E)
+    63, // input 122 (0x7A char 'z') => 63 (0x3F)
+    INVALID_VALUE, // input 123 (0x7B)
+    INVALID_VALUE, // input 124 (0x7C)
+    INVALID_VALUE, // input 125 (0x7D)
+    INVALID_VALUE, // input 126 (0x7E)
+    INVALID_VALUE, // input 127 (0x7F)
+    INVALID_VALUE, // input 128 (0x80)
+    INVALID_VALUE, // input 129 (0x81)
+    INVALID_VALUE, // input 130 (0x82)
+    INVALID_VALUE, // input 131 (0x83)
+    INVALID_VALUE, // input 132 (0x84)
+    INVALID_VALUE, // input 133 (0x85)
+    INVALID_VALUE, // input 134 (0x86)
+    INVALID_VALUE, // input 135 (0x87)
+    INVALID_VALUE, // input 136 (0x88)
+    INVALID_VALUE, // input 137 (0x89)
+    INVALID_VALUE, // input 138 (0x8A)
+    INVALID_VALUE, // input 139 (0x8B)
+    INVALID_VALUE, // input 140 (0x8C)
+    INVALID_VALUE, // input 141 (0x8D)
+    INVALID_VALUE, // input 142 (0x8E)
+    INVALID_VALUE, // input 143 (0x8F)
+    INVALID_VALUE, // input 144 (0x90)
+    INVALID_VALUE, // input 145 (0x91)
+    INVALID_VALUE, // input 146 (0x92)
+    INVALID_VALUE, // input 147 (0x93)
+    INVALID_VALUE, // input 148 (0x94)
+    INVALID_VALUE, // input 149 (0x95)
+    INVALID_VALUE, // input 150 (0x96)
+    INVALID_VALUE, // input 151 (0x97)
+    INVALID_VALUE, // input 152 (0x98)
+    INVALID_VALUE, // input 153 (0x99)
+    INVALID_VALUE, // input 154 (0x9A)
+    INVALID_VALUE, // input 155 (0x9B)
+    INVALID_VALUE, // input 156 (0x9C)
+    INVALID_VALUE, // input 157 (0x9D)
+    INVALID_VALUE, // input 158 (0x9E)
+    INVALID_VALUE, // input 159 (0x9F)
+    INVALID_VALUE, // input 160 (0xA0)
+    INVALID_VALUE, // input 161 (0xA1)
+    INVALID_VALUE, // input 162 (0xA2)
+    INVALID_VALUE, // input 163 (0xA3)
+    INVALID_VALUE, // input 164 (0xA4)
+    INVALID_VALUE, // input 165 (0xA5)
+    INVALID_VALUE, // input 166 (0xA6)
+    INVALID_VALUE, // input 167 (0xA7)
+    INVALID_VALUE, // input 168 (0xA8)
+    INVALID_VALUE, // input 169 (0xA9)
+    INVALID_VALUE, // input 170 (0xAA)
+    INVALID_VALUE, // input 171 (0xAB)
+    INVALID_VALUE, // input 172 (0xAC)
+    INVALID_VALUE, // input 173 (0xAD)
+    INVALID_VALUE, // input 174 (0xAE)
+    INVALID_VALUE, // input 175 (0xAF)
+    INVALID_VALUE, // input 176 (0xB0)
+    INVALID_VALUE, // input 177 (0xB1)
+    INVALID_VALUE, // input 178 (0xB2)
+    INVALID_VALUE, // input 179 (0xB3)
+    INVALID_VALUE, // input 180 (0xB4)
+    INVALID_VALUE, // input 181 (0xB5)
+    INVALID_VALUE, // input 182 (0xB6)
+    INVALID_VALUE, // input 183 (0xB7)
+    INVALID_VALUE, // input 184 (0xB8)
+    INVALID_VALUE, // input 185 (0xB9)
+    INVALID_VALUE, // input 186 (0xBA)
+    INVALID_VALUE, // input 187 (0xBB)
+    INVALID_VALUE, // input 188 (0xBC)
+    INVALID_VALUE, // input 189 (0xBD)
+    INVALID_VALUE, // input 190 (0xBE)
+    INVALID_VALUE, // input 191 (0xBF)
+    INVALID_VALUE, // input 192 (0xC0)
+    INVALID_VALUE, // input 193 (0xC1)
+    INVALID_VALUE, // input 194 (0xC2)
+    INVALID_VALUE, // input 195 (0xC3)
+    INVALID_VALUE, // input 196 (0xC4)
+    INVALID_VALUE, // input 197 (0xC5)
+    INVALID_VALUE, // input 198 (0xC6)
+    INVALID_VALUE, // input 199 (0xC7)
+    INVALID_VALUE, // input 200 (0xC8)
+    INVALID_VALUE, // input 201 (0xC9)
+    INVALID_VALUE, // input 202 (0xCA)
+    INVALID_VALUE, // input 203 (0xCB)
+    INVALID_VALUE, // input 204 (0xCC)
+    INVALID_VALUE, // input 205 (0xCD)
+    INVALID_VALUE, // input 206 (0xCE)
+    INVALID_VALUE, // input 207 (0xCF)
+    INVALID_VALUE, // input 208 (0xD0)
+    INVALID_VALUE, // input 209 (0xD1)
+    INVALID_VALUE, // input 210 (0xD2)
+    INVALID_VALUE, // input 211 (0xD3)
+    INVALID_VALUE, // input 212 (0xD4)
+    INVALID_VALUE, // input 213 (0xD5)
+    INVALID_VALUE, // input 214 (0xD6)
+    INVALID_VALUE, // input 215 (0xD7)
+    INVALID_VALUE, // input 216 (0xD8)
+    INVALID_VALUE, // input 217 (0xD9)
+    INVALID_VALUE, // input 218 (0xDA)
+    INVALID_VALUE, // input 219 (0xDB)
+    INVALID_VALUE, // input 220 (0xDC)
+    INVALID_VALUE, // input 221 (0xDD)
+    INVALID_VALUE, // input 222 (0xDE)
+    INVALID_VALUE, // input 223 (0xDF)
+    INVALID_VALUE, // input 224 (0xE0)
+    INVALID_VALUE, // input 225 (0xE1)
+    INVALID_VALUE, // input 226 (0xE2)
+    INVALID_VALUE, // input 227 (0xE3)
+    INVALID_VALUE, // input 228 (0xE4)
+    INVALID_VALUE, // input 229 (0xE5)
+    INVALID_VALUE, // input 230 (0xE6)
+    INVALID_VALUE, // input 231 (0xE7)
+    INVALID_VALUE, // input 232 (0xE8)
+    INVALID_VALUE, // input 233 (0xE9)
+    INVALID_VALUE, // input 234 (0xEA)
+    INVALID_VALUE, // input 235 (0xEB)
+    INVALID_VALUE, // input 236 (0xEC)
+    INVALID_VALUE, // input 237 (0xED)
+    INVALID_VALUE, // input 238 (0xEE)
+    INVALID_VALUE, // input 239 (0xEF)
+    INVALID_VALUE, // input 240 (0xF0)
+    INVALID_VALUE, // input 241 (0xF1)
+    INVALID_VALUE, // input 242 (0xF2)
+    INVALID_VALUE, // input 243 (0xF3)
+    INVALID_VALUE, // input 244 (0xF4)
+    INVALID_VALUE, // input 245 (0xF5)
+    INVALID_VALUE, // input 246 (0xF6)
+    INVALID_VALUE, // input 247 (0xF7)
+    INVALID_VALUE, // input 248 (0xF8)
+    INVALID_VALUE, // input 249 (0xF9)
+    INVALID_VALUE, // input 250 (0xFA)
+    INVALID_VALUE, // input 251 (0xFB)
+    INVALID_VALUE, // input 252 (0xFC)
+    INVALID_VALUE, // input 253 (0xFD)
+    INVALID_VALUE, // input 254 (0xFE)
+    INVALID_VALUE, // input 255 (0xFF)
+];
+#[rustfmt::skip]
+pub const BCRYPT_ENCODE: &[u8; 64] = &[
+    46, // input 0 (0x0) => '.' (0x2E)
+    47, // input 1 (0x1) => '/' (0x2F)
+    65, // input 2 (0x2) => 'A' (0x41)
+    66, // input 3 (0x3) => 'B' (0x42)
+    67, // input 4 (0x4) => 'C' (0x43)
+    68, // input 5 (0x5) => 'D' (0x44)
+    69, // input 6 (0x6) => 'E' (0x45)
+    70, // input 7 (0x7) => 'F' (0x46)
+    71, // input 8 (0x8) => 'G' (0x47)
+    72, // input 9 (0x9) => 'H' (0x48)
+    73, // input 10 (0xA) => 'I' (0x49)
+    74, // input 11 (0xB) => 'J' (0x4A)
+    75, // input 12 (0xC) => 'K' (0x4B)
+    76, // input 13 (0xD) => 'L' (0x4C)
+    77, // input 14 (0xE) => 'M' (0x4D)
+    78, // input 15 (0xF) => 'N' (0x4E)
+    79, // input 16 (0x10) => 'O' (0x4F)
+    80, // input 17 (0x11) => 'P' (0x50)
+    81, // input 18 (0x12) => 'Q' (0x51)
+    82, // input 19 (0x13) => 'R' (0x52)
+    83, // input 20 (0x14) => 'S' (0x53)
+    84, // input 21 (0x15) => 'T' (0x54)
+    85, // input 22 (0x16) => 'U' (0x55)
+    86, // input 23 (0x17) => 'V' (0x56)
+    87, // input 24 (0x18) => 'W' (0x57)
+    88, // input 25 (0x19) => 'X' (0x58)
+    89, // input 26 (0x1A) => 'Y' (0x59)
+    90, // input 27 (0x1B) => 'Z' (0x5A)
+    97, // input 28 (0x1C) => 'a' (0x61)
+    98, // input 29 (0x1D) => 'b' (0x62)
+    99, // input 30 (0x1E) => 'c' (0x63)
+    100, // input 31 (0x1F) => 'd' (0x64)
+    101, // input 32 (0x20) => 'e' (0x65)
+    102, // input 33 (0x21) => 'f' (0x66)
+    103, // input 34 (0x22) => 'g' (0x67)
+    104, // input 35 (0x23) => 'h' (0x68)
+    105, // input 36 (0x24) => 'i' (0x69)
+    106, // input 37 (0x25) => 'j' (0x6A)
+    107, // input 38 (0x26) => 'k' (0x6B)
+    108, // input 39 (0x27) => 'l' (0x6C)
+    109, // input 40 (0x28) => 'm' (0x6D)
+    110, // input 41 (0x29) => 'n' (0x6E)
+    111, // input 42 (0x2A) => 'o' (0x6F)
+    112, // input 43 (0x2B) => 'p' (0x70)
+    113, // input 44 (0x2C) => 'q' (0x71)
+    114, // input 45 (0x2D) => 'r' (0x72)
+    115, // input 46 (0x2E) => 's' (0x73)
+    116, // input 47 (0x2F) => 't' (0x74)
+    117, // input 48 (0x30) => 'u' (0x75)
+    118, // input 49 (0x31) => 'v' (0x76)
+    119, // input 50 (0x32) => 'w' (0x77)
+    120, // input 51 (0x33) => 'x' (0x78)
+    121, // input 52 (0x34) => 'y' (0x79)
+    122, // input 53 (0x35) => 'z' (0x7A)
+    48, // input 54 (0x36) => '0' (0x30)
+    49, // input 55 (0x37) => '1' (0x31)
+    50, // input 56 (0x38) => '2' (0x32)
+    51, // input 57 (0x39) => '3' (0x33)
+    52, // input 58 (0x3A) => '4' (0x34)
+    53, // input 59 (0x3B) => '5' (0x35)
+    54, // input 60 (0x3C) => '6' (0x36)
+    55, // input 61 (0x3D) => '7' (0x37)
+    56, // input 62 (0x3E) => '8' (0x38)
+    57, // input 63 (0x3F) => '9' (0x39)
+];
+#[rustfmt::skip]
+pub const BCRYPT_DECODE: &[u8; 256] = &[
+    INVALID_VALUE, // input 0 (0x0)
+    INVALID_VALUE, // input 1 (0x1)
+    INVALID_VALUE, // input 2 (0x2)
+    INVALID_VALUE, // input 3 (0x3)
+    INVALID_VALUE, // input 4 (0x4)
+    INVALID_VALUE, // input 5 (0x5)
+    INVALID_VALUE, // input 6 (0x6)
+    INVALID_VALUE, // input 7 (0x7)
+    INVALID_VALUE, // input 8 (0x8)
+    INVALID_VALUE, // input 9 (0x9)
+    INVALID_VALUE, // input 10 (0xA)
+    INVALID_VALUE, // input 11 (0xB)
+    INVALID_VALUE, // input 12 (0xC)
+    INVALID_VALUE, // input 13 (0xD)
+    INVALID_VALUE, // input 14 (0xE)
+    INVALID_VALUE, // input 15 (0xF)
+    INVALID_VALUE, // input 16 (0x10)
+    INVALID_VALUE, // input 17 (0x11)
+    INVALID_VALUE, // input 18 (0x12)
+    INVALID_VALUE, // input 19 (0x13)
+    INVALID_VALUE, // input 20 (0x14)
+    INVALID_VALUE, // input 21 (0x15)
+    INVALID_VALUE, // input 22 (0x16)
+    INVALID_VALUE, // input 23 (0x17)
+    INVALID_VALUE, // input 24 (0x18)
+    INVALID_VALUE, // input 25 (0x19)
+    INVALID_VALUE, // input 26 (0x1A)
+    INVALID_VALUE, // input 27 (0x1B)
+    INVALID_VALUE, // input 28 (0x1C)
+    INVALID_VALUE, // input 29 (0x1D)
+    INVALID_VALUE, // input 30 (0x1E)
+    INVALID_VALUE, // input 31 (0x1F)
+    INVALID_VALUE, // input 32 (0x20)
+    INVALID_VALUE, // input 33 (0x21)
+    INVALID_VALUE, // input 34 (0x22)
+    INVALID_VALUE, // input 35 (0x23)
+    INVALID_VALUE, // input 36 (0x24)
+    INVALID_VALUE, // input 37 (0x25)
+    INVALID_VALUE, // input 38 (0x26)
+    INVALID_VALUE, // input 39 (0x27)
+    INVALID_VALUE, // input 40 (0x28)
+    INVALID_VALUE, // input 41 (0x29)
+    INVALID_VALUE, // input 42 (0x2A)
+    INVALID_VALUE, // input 43 (0x2B)
+    INVALID_VALUE, // input 44 (0x2C)
+    INVALID_VALUE, // input 45 (0x2D)
+    0, // input 46 (0x2E char '.') => 0 (0x0)
+    1, // input 47 (0x2F char '/') => 1 (0x1)
+    54, // input 48 (0x30 char '0') => 54 (0x36)
+    55, // input 49 (0x31 char '1') => 55 (0x37)
+    56, // input 50 (0x32 char '2') => 56 (0x38)
+    57, // input 51 (0x33 char '3') => 57 (0x39)
+    58, // input 52 (0x34 char '4') => 58 (0x3A)
+    59, // input 53 (0x35 char '5') => 59 (0x3B)
+    60, // input 54 (0x36 char '6') => 60 (0x3C)
+    61, // input 55 (0x37 char '7') => 61 (0x3D)
+    62, // input 56 (0x38 char '8') => 62 (0x3E)
+    63, // input 57 (0x39 char '9') => 63 (0x3F)
+    INVALID_VALUE, // input 58 (0x3A)
+    INVALID_VALUE, // input 59 (0x3B)
+    INVALID_VALUE, // input 60 (0x3C)
+    INVALID_VALUE, // input 61 (0x3D)
+    INVALID_VALUE, // input 62 (0x3E)
+    INVALID_VALUE, // input 63 (0x3F)
+    INVALID_VALUE, // input 64 (0x40)
+    2, // input 65 (0x41 char 'A') => 2 (0x2)
+    3, // input 66 (0x42 char 'B') => 3 (0x3)
+    4, // input 67 (0x43 char 'C') => 4 (0x4)
+    5, // input 68 (0x44 char 'D') => 5 (0x5)
+    6, // input 69 (0x45 char 'E') => 6 (0x6)
+    7, // input 70 (0x46 char 'F') => 7 (0x7)
+    8, // input 71 (0x47 char 'G') => 8 (0x8)
+    9, // input 72 (0x48 char 'H') => 9 (0x9)
+    10, // input 73 (0x49 char 'I') => 10 (0xA)
+    11, // input 74 (0x4A char 'J') => 11 (0xB)
+    12, // input 75 (0x4B char 'K') => 12 (0xC)
+    13, // input 76 (0x4C char 'L') => 13 (0xD)
+    14, // input 77 (0x4D char 'M') => 14 (0xE)
+    15, // input 78 (0x4E char 'N') => 15 (0xF)
+    16, // input 79 (0x4F char 'O') => 16 (0x10)
+    17, // input 80 (0x50 char 'P') => 17 (0x11)
+    18, // input 81 (0x51 char 'Q') => 18 (0x12)
+    19, // input 82 (0x52 char 'R') => 19 (0x13)
+    20, // input 83 (0x53 char 'S') => 20 (0x14)
+    21, // input 84 (0x54 char 'T') => 21 (0x15)
+    22, // input 85 (0x55 char 'U') => 22 (0x16)
+    23, // input 86 (0x56 char 'V') => 23 (0x17)
+    24, // input 87 (0x57 char 'W') => 24 (0x18)
+    25, // input 88 (0x58 char 'X') => 25 (0x19)
+    26, // input 89 (0x59 char 'Y') => 26 (0x1A)
+    27, // input 90 (0x5A char 'Z') => 27 (0x1B)
+    INVALID_VALUE, // input 91 (0x5B)
+    INVALID_VALUE, // input 92 (0x5C)
+    INVALID_VALUE, // input 93 (0x5D)
+    INVALID_VALUE, // input 94 (0x5E)
+    INVALID_VALUE, // input 95 (0x5F)
+    INVALID_VALUE, // input 96 (0x60)
+    28, // input 97 (0x61 char 'a') => 28 (0x1C)
+    29, // input 98 (0x62 char 'b') => 29 (0x1D)
+    30, // input 99 (0x63 char 'c') => 30 (0x1E)
+    31, // input 100 (0x64 char 'd') => 31 (0x1F)
+    32, // input 101 (0x65 char 'e') => 32 (0x20)
+    33, // input 102 (0x66 char 'f') => 33 (0x21)
+    34, // input 103 (0x67 char 'g') => 34 (0x22)
+    35, // input 104 (0x68 char 'h') => 35 (0x23)
+    36, // input 105 (0x69 char 'i') => 36 (0x24)
+    37, // input 106 (0x6A char 'j') => 37 (0x25)
+    38, // input 107 (0x6B char 'k') => 38 (0x26)
+    39, // input 108 (0x6C char 'l') => 39 (0x27)
+    40, // input 109 (0x6D char 'm') => 40 (0x28)
+    41, // input 110 (0x6E char 'n') => 41 (0x29)
+    42, // input 111 (0x6F char 'o') => 42 (0x2A)
+    43, // input 112 (0x70 char 'p') => 43 (0x2B)
+    44, // input 113 (0x71 char 'q') => 44 (0x2C)
+    45, // input 114 (0x72 char 'r') => 45 (0x2D)
+    46, // input 115 (0x73 char 's') => 46 (0x2E)
+    47, // input 116 (0x74 char 't') => 47 (0x2F)
+    48, // input 117 (0x75 char 'u') => 48 (0x30)
+    49, // input 118 (0x76 char 'v') => 49 (0x31)
+    50, // input 119 (0x77 char 'w') => 50 (0x32)
+    51, // input 120 (0x78 char 'x') => 51 (0x33)
+    52, // input 121 (0x79 char 'y') => 52 (0x34)
+    53, // input 122 (0x7A char 'z') => 53 (0x35)
+    INVALID_VALUE, // input 123 (0x7B)
+    INVALID_VALUE, // input 124 (0x7C)
+    INVALID_VALUE, // input 125 (0x7D)
+    INVALID_VALUE, // input 126 (0x7E)
+    INVALID_VALUE, // input 127 (0x7F)
+    INVALID_VALUE, // input 128 (0x80)
+    INVALID_VALUE, // input 129 (0x81)
+    INVALID_VALUE, // input 130 (0x82)
+    INVALID_VALUE, // input 131 (0x83)
+    INVALID_VALUE, // input 132 (0x84)
+    INVALID_VALUE, // input 133 (0x85)
+    INVALID_VALUE, // input 134 (0x86)
+    INVALID_VALUE, // input 135 (0x87)
+    INVALID_VALUE, // input 136 (0x88)
+    INVALID_VALUE, // input 137 (0x89)
+    INVALID_VALUE, // input 138 (0x8A)
+    INVALID_VALUE, // input 139 (0x8B)
+    INVALID_VALUE, // input 140 (0x8C)
+    INVALID_VALUE, // input 141 (0x8D)
+    INVALID_VALUE, // input 142 (0x8E)
+    INVALID_VALUE, // input 143 (0x8F)
+    INVALID_VALUE, // input 144 (0x90)
+    INVALID_VALUE, // input 145 (0x91)
+    INVALID_VALUE, // input 146 (0x92)
+    INVALID_VALUE, // input 147 (0x93)
+    INVALID_VALUE, // input 148 (0x94)
+    INVALID_VALUE, // input 149 (0x95)
+    INVALID_VALUE, // input 150 (0x96)
+    INVALID_VALUE, // input 151 (0x97)
+    INVALID_VALUE, // input 152 (0x98)
+    INVALID_VALUE, // input 153 (0x99)
+    INVALID_VALUE, // input 154 (0x9A)
+    INVALID_VALUE, // input 155 (0x9B)
+    INVALID_VALUE, // input 156 (0x9C)
+    INVALID_VALUE, // input 157 (0x9D)
+    INVALID_VALUE, // input 158 (0x9E)
+    INVALID_VALUE, // input 159 (0x9F)
+    INVALID_VALUE, // input 160 (0xA0)
+    INVALID_VALUE, // input 161 (0xA1)
+    INVALID_VALUE, // input 162 (0xA2)
+    INVALID_VALUE, // input 163 (0xA3)
+    INVALID_VALUE, // input 164 (0xA4)
+    INVALID_VALUE, // input 165 (0xA5)
+    INVALID_VALUE, // input 166 (0xA6)
+    INVALID_VALUE, // input 167 (0xA7)
+    INVALID_VALUE, // input 168 (0xA8)
+    INVALID_VALUE, // input 169 (0xA9)
+    INVALID_VALUE, // input 170 (0xAA)
+    INVALID_VALUE, // input 171 (0xAB)
+    INVALID_VALUE, // input 172 (0xAC)
+    INVALID_VALUE, // input 173 (0xAD)
+    INVALID_VALUE, // input 174 (0xAE)
+    INVALID_VALUE, // input 175 (0xAF)
+    INVALID_VALUE, // input 176 (0xB0)
+    INVALID_VALUE, // input 177 (0xB1)
+    INVALID_VALUE, // input 178 (0xB2)
+    INVALID_VALUE, // input 179 (0xB3)
+    INVALID_VALUE, // input 180 (0xB4)
+    INVALID_VALUE, // input 181 (0xB5)
+    INVALID_VALUE, // input 182 (0xB6)
+    INVALID_VALUE, // input 183 (0xB7)
+    INVALID_VALUE, // input 184 (0xB8)
+    INVALID_VALUE, // input 185 (0xB9)
+    INVALID_VALUE, // input 186 (0xBA)
+    INVALID_VALUE, // input 187 (0xBB)
+    INVALID_VALUE, // input 188 (0xBC)
+    INVALID_VALUE, // input 189 (0xBD)
+    INVALID_VALUE, // input 190 (0xBE)
+    INVALID_VALUE, // input 191 (0xBF)
+    INVALID_VALUE, // input 192 (0xC0)
+    INVALID_VALUE, // input 193 (0xC1)
+    INVALID_VALUE, // input 194 (0xC2)
+    INVALID_VALUE, // input 195 (0xC3)
+    INVALID_VALUE, // input 196 (0xC4)
+    INVALID_VALUE, // input 197 (0xC5)
+    INVALID_VALUE, // input 198 (0xC6)
+    INVALID_VALUE, // input 199 (0xC7)
+    INVALID_VALUE, // input 200 (0xC8)
+    INVALID_VALUE, // input 201 (0xC9)
+    INVALID_VALUE, // input 202 (0xCA)
+    INVALID_VALUE, // input 203 (0xCB)
+    INVALID_VALUE, // input 204 (0xCC)
+    INVALID_VALUE, // input 205 (0xCD)
+    INVALID_VALUE, // input 206 (0xCE)
+    INVALID_VALUE, // input 207 (0xCF)
+    INVALID_VALUE, // input 208 (0xD0)
+    INVALID_VALUE, // input 209 (0xD1)
+    INVALID_VALUE, // input 210 (0xD2)
+    INVALID_VALUE, // input 211 (0xD3)
+    INVALID_VALUE, // input 212 (0xD4)
+    INVALID_VALUE, // input 213 (0xD5)
+    INVALID_VALUE, // input 214 (0xD6)
+    INVALID_VALUE, // input 215 (0xD7)
+    INVALID_VALUE, // input 216 (0xD8)
+    INVALID_VALUE, // input 217 (0xD9)
+    INVALID_VALUE, // input 218 (0xDA)
+    INVALID_VALUE, // input 219 (0xDB)
+    INVALID_VALUE, // input 220 (0xDC)
+    INVALID_VALUE, // input 221 (0xDD)
+    INVALID_VALUE, // input 222 (0xDE)
+    INVALID_VALUE, // input 223 (0xDF)
+    INVALID_VALUE, // input 224 (0xE0)
+    INVALID_VALUE, // input 225 (0xE1)
+    INVALID_VALUE, // input 226 (0xE2)
+    INVALID_VALUE, // input 227 (0xE3)
+    INVALID_VALUE, // input 228 (0xE4)
+    INVALID_VALUE, // input 229 (0xE5)
+    INVALID_VALUE, // input 230 (0xE6)
+    INVALID_VALUE, // input 231 (0xE7)
+    INVALID_VALUE, // input 232 (0xE8)
+    INVALID_VALUE, // input 233 (0xE9)
+    INVALID_VALUE, // input 234 (0xEA)
+    INVALID_VALUE, // input 235 (0xEB)
+    INVALID_VALUE, // input 236 (0xEC)
+    INVALID_VALUE, // input 237 (0xED)
+    INVALID_VALUE, // input 238 (0xEE)
+    INVALID_VALUE, // input 239 (0xEF)
+    INVALID_VALUE, // input 240 (0xF0)
+    INVALID_VALUE, // input 241 (0xF1)
+    INVALID_VALUE, // input 242 (0xF2)
+    INVALID_VALUE, // input 243 (0xF3)
+    INVALID_VALUE, // input 244 (0xF4)
+    INVALID_VALUE, // input 245 (0xF5)
+    INVALID_VALUE, // input 246 (0xF6)
+    INVALID_VALUE, // input 247 (0xF7)
+    INVALID_VALUE, // input 248 (0xF8)
+    INVALID_VALUE, // input 249 (0xF9)
+    INVALID_VALUE, // input 250 (0xFA)
+    INVALID_VALUE, // input 251 (0xFB)
+    INVALID_VALUE, // input 252 (0xFC)
+    INVALID_VALUE, // input 253 (0xFD)
+    INVALID_VALUE, // input 254 (0xFE)
+    INVALID_VALUE, // input 255 (0xFF)
+];
+#[rustfmt::skip]
+pub const IMAP_MUTF7_ENCODE: &[u8; 64] = &[
+    65, // input 0 (0x0) => 'A' (0x41)
+    66, // input 1 (0x1) => 'B' (0x42)
+    67, // input 2 (0x2) => 'C' (0x43)
+    68, // input 3 (0x3) => 'D' (0x44)
+    69, // input 4 (0x4) => 'E' (0x45)
+    70, // input 5 (0x5) => 'F' (0x46)
+    71, // input 6 (0x6) => 'G' (0x47)
+    72, // input 7 (0x7) => 'H' (0x48)
+    73, // input 8 (0x8) => 'I' (0x49)
+    74, // input 9 (0x9) => 'J' (0x4A)
+    75, // input 10 (0xA) => 'K' (0x4B)
+    76, // input 11 (0xB) => 'L' (0x4C)
+    77, // input 12 (0xC) => 'M' (0x4D)
+    78, // input 13 (0xD) => 'N' (0x4E)
+    79, // input 14 (0xE) => 'O' (0x4F)
+    80, // input 15 (0xF) => 'P' (0x50)
+    81, // input 16 (0x10) => 'Q' (0x51)
+    82, // input 17 (0x11) => 'R' (0x52)
+    83, // input 18 (0x12) => 'S' (0x53)
+    84, // input 19 (0x13) => 'T' (0x54)
+    85, // input 20 (0x14) => 'U' (0x55)
+    86, // input 21 (0x15) => 'V' (0x56)
+    87, // input 22 (0x16) => 'W' (0x57)
+    88, // input 23 (0x17) => 'X' (0x58)
+    89, // input 24 (0x18) => 'Y' (0x59)
+    90, // input 25 (0x19) => 'Z' (0x5A)
+    97, // input 26 (0x1A) => 'a' (0x61)
+    98, // input 27 (0x1B) => 'b' (0x62)
+    99, // input 28 (0x1C) => 'c' (0x63)
+    100, // input 29 (0x1D) => 'd' (0x64)
+    101, // input 30 (0x1E) => 'e' (0x65)
+    102, // input 31 (0x1F) => 'f' (0x66)
+    103, // input 32 (0x20) => 'g' (0x67)
+    104, // input 33 (0x21) => 'h' (0x68)
+    105, // input 34 (0x22) => 'i' (0x69)
+    106, // input 35 (0x23) => 'j' (0x6A)
+    107, // input 36 (0x24) => 'k' (0x6B)
+    108, // input 37 (0x25) => 'l' (0x6C)
+    109, // input 38 (0x26) => 'm' (0x6D)
+    110, // input 39 (0x27) => 'n' (0x6E)
+    111, // input 40 (0x28) => 'o' (0x6F)
+    112, // input 41 (0x29) => 'p' (0x70)
+    113, // input 42 (0x2A) => 'q' (0x71)
+    114, // input 43 (0x2B) => 'r' (0x72)
+    115, // input 44 (0x2C) => 's' (0x73)
+    116, // input 45 (0x2D) => 't' (0x74)
+    117, // input 46 (0x2E) => 'u' (0x75)
+    118, // input 47 (0x2F) => 'v' (0x76)
+    119, // input 48 (0x30) => 'w' (0x77)
+    120, // input 49 (0x31) => 'x' (0x78)
+    121, // input 50 (0x32) => 'y' (0x79)
+    122, // input 51 (0x33) => 'z' (0x7A)
+    48, // input 52 (0x34) => '0' (0x30)
+    49, // input 53 (0x35) => '1' (0x31)
+    50, // input 54 (0x36) => '2' (0x32)
+    51, // input 55 (0x37) => '3' (0x33)
+    52, // input 56 (0x38) => '4' (0x34)
+    53, // input 57 (0x39) => '5' (0x35)
+    54, // input 58 (0x3A) => '6' (0x36)
+    55, // input 59 (0x3B) => '7' (0x37)
+    56, // input 60 (0x3C) => '8' (0x38)
+    57, // input 61 (0x3D) => '9' (0x39)
+    43, // input 62 (0x3E) => '+' (0x2B)
+    44, // input 63 (0x3F) => ',' (0x2C)
+];
+#[rustfmt::skip]
+pub const IMAP_MUTF7_DECODE: &[u8; 256] = &[
+    INVALID_VALUE, // input 0 (0x0)
+    INVALID_VALUE, // input 1 (0x1)
+    INVALID_VALUE, // input 2 (0x2)
+    INVALID_VALUE, // input 3 (0x3)
+    INVALID_VALUE, // input 4 (0x4)
+    INVALID_VALUE, // input 5 (0x5)
+    INVALID_VALUE, // input 6 (0x6)
+    INVALID_VALUE, // input 7 (0x7)
+    INVALID_VALUE, // input 8 (0x8)
+    INVALID_VALUE, // input 9 (0x9)
+    INVALID_VALUE, // input 10 (0xA)
+    INVALID_VALUE, // input 11 (0xB)
+    INVALID_VALUE, // input 12 (0xC)
+    INVALID_VALUE, // input 13 (0xD)
+    INVALID_VALUE, // input 14 (0xE)
+    INVALID_VALUE, // input 15 (0xF)
+    INVALID_VALUE, // input 16 (0x10)
+    INVALID_VALUE, // input 17 (0x11)
+    INVALID_VALUE, // input 18 (0x12)
+    INVALID_VALUE, // input 19 (0x13)
+    INVALID_VALUE, // input 20 (0x14)
+    INVALID_VALUE, // input 21 (0x15)
+    INVALID_VALUE, // input 22 (0x16)
+    INVALID_VALUE, // input 23 (0x17)
+    INVALID_VALUE, // input 24 (0x18)
+    INVALID_VALUE, // input 25 (0x19)
+    INVALID_VALUE, // input 26 (0x1A)
+    INVALID_VALUE, // input 27 (0x1B)
+    INVALID_VALUE, // input 28 (0x1C)
+    INVALID_VALUE, // input 29 (0x1D)
+    INVALID_VALUE, // input 30 (0x1E)
+    INVALID_VALUE, // input 31 (0x1F)
+    INVALID_VALUE, // input 32 (0x20)
+    INVALID_VALUE, // input 33 (0x21)
+    INVALID_VALUE, // input 34 (0x22)
+    INVALID_VALUE, // input 35 (0x23)
+    INVALID_VALUE, // input 36 (0x24)
+    INVALID_VALUE, // input 37 (0x25)
+    INVALID_VALUE, // input 38 (0x26)
+    INVALID_VALUE, // input 39 (0x27)
+    INVALID_VALUE, // input 40 (0x28)
+    INVALID_VALUE, // input 41 (0x29)
+    INVALID_VALUE, // input 42 (0x2A)
+    62, // input 43 (0x2B char '+') => 62 (0x3E)
+    63, // input 44 (0x2C char ',') => 63 (0x3F)
+    INVALID_VALUE, // input 45 (0x2D)
+    INVALID_VALUE, // input 46 (0x2E)
+    INVALID_VALUE, // input 47 (0x2F)
+    52, // input 48 (0x30 char '0') => 52 (0x34)
+    53, // input 49 (0x31 char '1') => 53 (0x35)
+    54, // input 50 (0x32 char '2') => 54 (0x36)
+    55, // input 51 (0x33 char '3') => 55 (0x37)
+    56, // input 52 (0x34 char '4') => 56 (0x38)
+    57, // input 53 (0x35 char '5') => 57 (0x39)
+    58, // input 54 (0x36 char '6') => 58 (0x3A)
+    59, // input 55 (0x37 char '7') => 59 (0x3B)
+    60, // input 56 (0x38 char '8') => 60 (0x3C)
+    61, // input 57 (0x39 char '9') => 61 (0x3D)
+    INVALID_VALUE, // input 58 (0x3A)
+    INVALID_VALUE, // input 59 (0x3B)
+    INVALID_VALUE, // input 60 (0x3C)
+    INVALID_VALUE, // input 61 (0x3D)
+    INVALID_VALUE, // input 62 (0x3E)
+    INVALID_VALUE, // input 63 (0x3F)
+    INVALID_VALUE, // input 64 (0x40)
+    0, // input 65 (0x41 char 'A') => 0 (0x0)
+    1, // input 66 (0x42 char 'B') => 1 (0x1)
+    2, // input 67 (0x43 char 'C') => 2 (0x2)
+    3, // input 68 (0x44 char 'D') => 3 (0x3)
+    4, // input 69 (0x45 char 'E') => 4 (0x4)
+    5, // input 70 (0x46 char 'F') => 5 (0x5)
+    6, // input 71 (0x47 char 'G') => 6 (0x6)
+    7, // input 72 (0x48 char 'H') => 7 (0x7)
+    8, // input 73 (0x49 char 'I') => 8 (0x8)
+    9, // input 74 (0x4A char 'J') => 9 (0x9)
+    10, // input 75 (0x4B char 'K') => 10 (0xA)
+    11, // input 76 (0x4C char 'L') => 11 (0xB)
+    12, // input 77 (0x4D char 'M') => 12 (0xC)
+    13, // input 78 (0x4E char 'N') => 13 (0xD)
+    14, // input 79 (0x4F char 'O') => 14 (0xE)
+    15, // input 80 (0x50 char 'P') => 15 (0xF)
+    16, // input 81 (0x51 char 'Q') => 16 (0x10)
+    17, // input 82 (0x52 char 'R') => 17 (0x11)
+    18, // input 83 (0x53 char 'S') => 18 (0x12)
+    19, // input 84 (0x54 char 'T') => 19 (0x13)
+    20, // input 85 (0x55 char 'U') => 20 (0x14)
+    21, // input 86 (0x56 char 'V') => 21 (0x15)
+    22, // input 87 (0x57 char 'W') => 22 (0x16)
+    23, // input 88 (0x58 char 'X') => 23 (0x17)
+    24, // input 89 (0x59 char 'Y') => 24 (0x18)
+    25, // input 90 (0x5A char 'Z') => 25 (0x19)
+    INVALID_VALUE, // input 91 (0x5B)
+    INVALID_VALUE, // input 92 (0x5C)
+    INVALID_VALUE, // input 93 (0x5D)
+    INVALID_VALUE, // input 94 (0x5E)
+    INVALID_VALUE, // input 95 (0x5F)
+    INVALID_VALUE, // input 96 (0x60)
+    26, // input 97 (0x61 char 'a') => 26 (0x1A)
+    27, // input 98 (0x62 char 'b') => 27 (0x1B)
+    28, // input 99 (0x63 char 'c') => 28 (0x1C)
+    29, // input 100 (0x64 char 'd') => 29 (0x1D)
+    30, // input 101 (0x65 char 'e') => 30 (0x1E)
+    31, // input 102 (0x66 char 'f') => 31 (0x1F)
+    32, // input 103 (0x67 char 'g') => 32 (0x20)
+    33, // input 104 (0x68 char 'h') => 33 (0x21)
+    34, // input 105 (0x69 char 'i') => 34 (0x22)
+    35, // input 106 (0x6A char 'j') => 35 (0x23)
+    36, // input 107 (0x6B char 'k') => 36 (0x24)
+    37, // input 108 (0x6C char 'l') => 37 (0x25)
+    38, // input 109 (0x6D char 'm') => 38 (0x26)
+    39, // input 110 (0x6E char 'n') => 39 (0x27)
+    40, // input 111 (0x6F char 'o') => 40 (0x28)
+    41, // input 112 (0x70 char 'p') => 41 (0x29)
+    42, // input 113 (0x71 char 'q') => 42 (0x2A)
+    43, // input 114 (0x72 char 'r') => 43 (0x2B)
+    44, // input 115 (0x73 char 's') => 44 (0x2C)
+    45, // input 116 (0x74 char 't') => 45 (0x2D)
+    46, // input 117 (0x75 char 'u') => 46 (0x2E)
+    47, // input 118 (0x76 char 'v') => 47 (0x2F)
+    48, // input 119 (0x77 char 'w') => 48 (0x30)
+    49, // input 120 (0x78 char 'x') => 49 (0x31)
+    50, // input 121 (0x79 char 'y') => 50 (0x32)
+    51, // input 122 (0x7A char 'z') => 51 (0x33)
+    INVALID_VALUE, // input 123 (0x7B)
+    INVALID_VALUE, // input 124 (0x7C)
+    INVALID_VALUE, // input 125 (0x7D)
+    INVALID_VALUE, // input 126 (0x7E)
+    INVALID_VALUE, // input 127 (0x7F)
+    INVALID_VALUE, // input 128 (0x80)
+    INVALID_VALUE, // input 129 (0x81)
+    INVALID_VALUE, // input 130 (0x82)
+    INVALID_VALUE, // input 131 (0x83)
+    INVALID_VALUE, // input 132 (0x84)
+    INVALID_VALUE, // input 133 (0x85)
+    INVALID_VALUE, // input 134 (0x86)
+    INVALID_VALUE, // input 135 (0x87)
+    INVALID_VALUE, // input 136 (0x88)
+    INVALID_VALUE, // input 137 (0x89)
+    INVALID_VALUE, // input 138 (0x8A)
+    INVALID_VALUE, // input 139 (0x8B)
+    INVALID_VALUE, // input 140 (0x8C)
+    INVALID_VALUE, // input 141 (0x8D)
+    INVALID_VALUE, // input 142 (0x8E)
+    INVALID_VALUE, // input 143 (0x8F)
+    INVALID_VALUE, // input 144 (0x90)
+    INVALID_VALUE, // input 145 (0x91)
+    INVALID_VALUE, // input 146 (0x92)
+    INVALID_VALUE, // input 147 (0x93)
+    INVALID_VALUE, // input 148 (0x94)
+    INVALID_VALUE, // input 149 (0x95)
+    INVALID_VALUE, // input 150 (0x96)
+    INVALID_VALUE, // input 151 (0x97)
+    INVALID_VALUE, // input 152 (0x98)
+    INVALID_VALUE, // input 153 (0x99)
+    INVALID_VALUE, // input 154 (0x9A)
+    INVALID_VALUE, // input 155 (0x9B)
+    INVALID_VALUE, // input 156 (0x9C)
+    INVALID_VALUE, // input 157 (0x9D)
+    INVALID_VALUE, // input 158 (0x9E)
+    INVALID_VALUE, // input 159 (0x9F)
+    INVALID_VALUE, // input 160 (0xA0)
+    INVALID_VALUE, // input 161 (0xA1)
+    INVALID_VALUE, // input 162 (0xA2)
+    INVALID_VALUE, // input 163 (0xA3)
+    INVALID_VALUE, // input 164 (0xA4)
+    INVALID_VALUE, // input 165 (0xA5)
+    INVALID_VALUE, // input 166 (0xA6)
+    INVALID_VALUE, // input 167 (0xA7)
+    INVALID_VALUE, // input 168 (0xA8)
+    INVALID_VALUE, // input 169 (0xA9)
+    INVALID_VALUE, // input 170 (0xAA)
+    INVALID_VALUE, // input 171 (0xAB)
+    INVALID_VALUE, // input 172 (0xAC)
+    INVALID_VALUE, // input 173 (0xAD)
+    INVALID_VALUE, // input 174 (0xAE)
+    INVALID_VALUE, // input 175 (0xAF)
+    INVALID_VALUE, // input 176 (0xB0)
+    INVALID_VALUE, // input 177 (0xB1)
+    INVALID_VALUE, // input 178 (0xB2)
+    INVALID_VALUE, // input 179 (0xB3)
+    INVALID_VALUE, // input 180 (0xB4)
+    INVALID_VALUE, // input 181 (0xB5)
+    INVALID_VALUE, // input 182 (0xB6)
+    INVALID_VALUE, // input 183 (0xB7)
+    INVALID_VALUE, // input 184 (0xB8)
+    INVALID_VALUE, // input 185 (0xB9)
+    INVALID_VALUE, // input 186 (0xBA)
+    INVALID_VALUE, // input 187 (0xBB)
+    INVALID_VALUE, // input 188 (0xBC)
+    INVALID_VALUE, // input 189 (0xBD)
+    INVALID_VALUE, // input 190 (0xBE)
+    INVALID_VALUE, // input 191 (0xBF)
+    INVALID_VALUE, // input 192 (0xC0)
+    INVALID_VALUE, // input 193 (0xC1)
+    INVALID_VALUE, // input 194 (0xC2)
+    INVALID_VALUE, // input 195 (0xC3)
+    INVALID_VALUE, // input 196 (0xC4)
+    INVALID_VALUE, // input 197 (0xC5)
+    INVALID_VALUE, // input 198 (0xC6)
+    INVALID_VALUE, // input 199 (0xC7)
+    INVALID_VALUE, // input 200 (0xC8)
+    INVALID_VALUE, // input 201 (0xC9)
+    INVALID_VALUE, // input 202 (0xCA)
+    INVALID_VALUE, // input 203 (0xCB)
+    INVALID_VALUE, // input 204 (0xCC)
+    INVALID_VALUE, // input 205 (0xCD)
+    INVALID_VALUE, // input 206 (0xCE)
+    INVALID_VALUE, // input 207 (0xCF)
+    INVALID_VALUE, // input 208 (0xD0)
+    INVALID_VALUE, // input 209 (0xD1)
+    INVALID_VALUE, // input 210 (0xD2)
+    INVALID_VALUE, // input 211 (0xD3)
+    INVALID_VALUE, // input 212 (0xD4)
+    INVALID_VALUE, // input 213 (0xD5)
+    INVALID_VALUE, // input 214 (0xD6)
+    INVALID_VALUE, // input 215 (0xD7)
+    INVALID_VALUE, // input 216 (0xD8)
+    INVALID_VALUE, // input 217 (0xD9)
+    INVALID_VALUE, // input 218 (0xDA)
+    INVALID_VALUE, // input 219 (0xDB)
+    INVALID_VALUE, // input 220 (0xDC)
+    INVALID_VALUE, // input 221 (0xDD)
+    INVALID_VALUE, // input 222 (0xDE)
+    INVALID_VALUE, // input 223 (0xDF)
+    INVALID_VALUE, // input 224 (0xE0)
+    INVALID_VALUE, // input 225 (0xE1)
+    INVALID_VALUE, // input 226 (0xE2)
+    INVALID_VALUE, // input 227 (0xE3)
+    INVALID_VALUE, // input 228 (0xE4)
+    INVALID_VALUE, // input 229 (0xE5)
+    INVALID_VALUE, // input 230 (0xE6)
+    INVALID_VALUE, // input 231 (0xE7)
+    INVALID_VALUE, // input 232 (0xE8)
+    INVALID_VALUE, // input 233 (0xE9)
+    INVALID_VALUE, // input 234 (0xEA)
+    INVALID_VALUE, // input 235 (0xEB)
+    INVALID_VALUE, // input 236 (0xEC)
+    INVALID_VALUE, // input 237 (0xED)
+    INVALID_VALUE, // input 238 (0xEE)
+    INVALID_VALUE, // input 239 (0xEF)
+    INVALID_VALUE, // input 240 (0xF0)
+    INVALID_VALUE, // input 241 (0xF1)
+    INVALID_VALUE, // input 242 (0xF2)
+    INVALID_VALUE, // input 243 (0xF3)
+    INVALID_VALUE, // input 244 (0xF4)
+    INVALID_VALUE, // input 245 (0xF5)
+    INVALID_VALUE, // input 246 (0xF6)
+    INVALID_VALUE, // input 247 (0xF7)
+    INVALID_VALUE, // input 248 (0xF8)
+    INVALID_VALUE, // input 249 (0xF9)
+    INVALID_VALUE, // input 250 (0xFA)
+    INVALID_VALUE, // input 251 (0xFB)
+    INVALID_VALUE, // input 252 (0xFC)
+    INVALID_VALUE, // input 253 (0xFD)
+    INVALID_VALUE, // input 254 (0xFE)
+    INVALID_VALUE, // input 255 (0xFF)
+];
+#[rustfmt::skip]
+pub const BINHEX_ENCODE: &[u8; 64] = &[
+    33, // input 0 (0x0) => '!' (0x21)
+    34, // input 1 (0x1) => '"' (0x22)
+    35, // input 2 (0x2) => '#' (0x23)
+    36, // input 3 (0x3) => '$' (0x24)
+    37, // input 4 (0x4) => '%' (0x25)
+    38, // input 5 (0x5) => '&' (0x26)
+    39, // input 6 (0x6) => ''' (0x27)
+    40, // input 7 (0x7) => '(' (0x28)
+    41, // input 8 (0x8) => ')' (0x29)
+    42, // input 9 (0x9) => '*' (0x2A)
+    43, // input 10 (0xA) => '+' (0x2B)
+    44, // input 11 (0xB) => ',' (0x2C)
+    45, // input 12 (0xC) => '-' (0x2D)
+    48, // input 13 (0xD) => '0' (0x30)
+    49, // input 14 (0xE) => '1' (0x31)
+    50, // input 15 (0xF) => '2' (0x32)
+    51, // input 16 (0x10) => '3' (0x33)
+    52, // input 17 (0x11) => '4' (0x34)
+    53, // input 18 (0x12) => '5' (0x35)
+    54, // input 19 (0x13) => '6' (0x36)
+    55, // input 20 (0x14) => '7' (0x37)
+    56, // input 21 (0x15) => '8' (0x38)
+    57, // input 22 (0x16) => '9' (0x39)
+    64, // input 23 (0x17) => '@' (0x40)
+    65, // input 24 (0x18) => 'A' (0x41)
+    66, // input 25 (0x19) => 'B' (0x42)
+    67, // input 26 (0x1A) => 'C' (0x43)
+    68, // input 27 (0x1B) => 'D' (0x44)
+    69, // input 28 (0x1C) => 'E' (0x45)
+    70, // input 29 (0x1D) => 'F' (0x46)
+    71, // input 30 (0x1E) => 'G' (0x47)
+    72, // input 31 (0x1F) => 'H' (0x48)
+    73, // input 32 (0x20) => 'I' (0x49)
+    74, // input 33 (0x21) => 'J' (0x4A)
+    75, // input 34 (0x22) => 'K' (0x4B)
+    76, // input 35 (0x23) => 'L' (0x4C)
+    77, // input 36 (0x24) => 'M' (0x4D)
+    78, // input 37 (0x25) => 'N' (0x4E)
+    80, // input 38 (0x26) => 'P' (0x50)
+    81, // input 39 (0x27) => 'Q' (0x51)
+    82, // input 40 (0x28) => 'R' (0x52)
+    83, // input 41 (0x29) => 'S' (0x53)
+    84, // input 42 (0x2A) => 'T' (0x54)
+    85, // input 43 (0x2B) => 'U' (0x55)
+    86, // input 44 (0x2C) => 'V' (0x56)
+    88, // input 45 (0x2D) => 'X' (0x58)
+    89, // input 46 (0x2E) => 'Y' (0x59)
+    90, // input 47 (0x2F) => 'Z' (0x5A)
+    91, // input 48 (0x30) => '[' (0x5B)
+    96, // input 49 (0x31) => '`' (0x60)
+    97, // input 50 (0x32) => 'a' (0x61)
+    98, // input 51 (0x33) => 'b' (0x62)
+    99, // input 52 (0x34) => 'c' (0x63)
+    100, // input 53 (0x35) => 'd' (0x64)
+    101, // input 54 (0x36) => 'e' (0x65)
+    104, // input 55 (0x37) => 'h' (0x68)
+    105, // input 56 (0x38) => 'i' (0x69)
+    106, // input 57 (0x39) => 'j' (0x6A)
+    107, // input 58 (0x3A) => 'k' (0x6B)
+    108, // input 59 (0x3B) => 'l' (0x6C)
+    109, // input 60 (0x3C) => 'm' (0x6D)
+    112, // input 61 (0x3D) => 'p' (0x70)
+    113, // input 62 (0x3E) => 'q' (0x71)
+    114, // input 63 (0x3F) => 'r' (0x72)
+];
+#[rustfmt::skip]
+pub const BINHEX_DECODE: &[u8; 256] = &[
+    INVALID_VALUE, // input 0 (0x0)
+    INVALID_VALUE, // input 1 (0x1)
+    INVALID_VALUE, // input 2 (0x2)
+    INVALID_VALUE, // input 3 (0x3)
+    INVALID_VALUE, // input 4 (0x4)
+    INVALID_VALUE, // input 5 (0x5)
+    INVALID_VALUE, // input 6 (0x6)
+    INVALID_VALUE, // input 7 (0x7)
+    INVALID_VALUE, // input 8 (0x8)
+    INVALID_VALUE, // input 9 (0x9)
+    INVALID_VALUE, // input 10 (0xA)
+    INVALID_VALUE, // input 11 (0xB)
+    INVALID_VALUE, // input 12 (0xC)
+    INVALID_VALUE, // input 13 (0xD)
+    INVALID_VALUE, // input 14 (0xE)
+    INVALID_VALUE, // input 15 (0xF)
+    INVALID_VALUE, // input 16 (0x10)
+    INVALID_VALUE, // input 17 (0x11)
+    INVALID_VALUE, // input 18 (0x12)
+    INVALID_VALUE, // input 19 (0x13)
+    INVALID_VALUE, // input 20 (0x14)
+    INVALID_VALUE, // input 21 (0x15)
+    INVALID_VALUE, // input 22 (0x16)
+    INVALID_VALUE, // input 23 (0x17)
+    INVALID_VALUE, // input 24 (0x18)
+    INVALID_VALUE, // input 25 (0x19)
+    INVALID_VALUE, // input 26 (0x1A)
+    INVALID_VALUE, // input 27 (0x1B)
+    INVALID_VALUE, // input 28 (0x1C)
+    INVALID_VALUE, // input 29 (0x1D)
+    INVALID_VALUE, // input 30 (0x1E)
+    INVALID_VALUE, // input 31 (0x1F)
+    INVALID_VALUE, // input 32 (0x20)
+    0, // input 33 (0x21 char '!') => 0 (0x0)
+    1, // input 34 (0x22 char '"') => 1 (0x1)
+    2, // input 35 (0x23 char '#') => 2 (0x2)
+    3, // input 36 (0x24 char '$') => 3 (0x3)
+    4, // input 37 (0x25 char '%') => 4 (0x4)
+    5, // input 38 (0x26 char '&') => 5 (0x5)
+    6, // input 39 (0x27 char ''') => 6 (0x6)
+    7, // input 40 (0x28 char '(') => 7 (0x7)
+    8, // input 41 (0x29 char ')') => 8 (0x8)
+    9, // input 42 (0x2A char '*') => 9 (0x9)
+    10, // input 43 (0x2B char '+') => 10 (0xA)
+    11, // input 44 (0x2C char ',') => 11 (0xB)
+    12, // input 45 (0x2D char '-') => 12 (0xC)
+    INVALID_VALUE, // input 46 (0x2E)
+    INVALID_VALUE, // input 47 (0x2F)
+    13, // input 48 (0x30 char '0') => 13 (0xD)
+    14, // input 49 (0x31 char '1') => 14 (0xE)
+    15, // input 50 (0x32 char '2') => 15 (0xF)
+    16, // input 51 (0x33 char '3') => 16 (0x10)
+    17, // input 52 (0x34 char '4') => 17 (0x11)
+    18, // input 53 (0x35 char '5') => 18 (0x12)
+    19, // input 54 (0x36 char '6') => 19 (0x13)
+    20, // input 55 (0x37 char '7') => 20 (0x14)
+    21, // input 56 (0x38 char '8') => 21 (0x15)
+    22, // input 57 (0x39 char '9') => 22 (0x16)
+    INVALID_VALUE, // input 58 (0x3A)
+    INVALID_VALUE, // input 59 (0x3B)
+    INVALID_VALUE, // input 60 (0x3C)
+    INVALID_VALUE, // input 61 (0x3D)
+    INVALID_VALUE, // input 62 (0x3E)
+    INVALID_VALUE, // input 63 (0x3F)
+    23, // input 64 (0x40 char '@') => 23 (0x17)
+    24, // input 65 (0x41 char 'A') => 24 (0x18)
+    25, // input 66 (0x42 char 'B') => 25 (0x19)
+    26, // input 67 (0x43 char 'C') => 26 (0x1A)
+    27, // input 68 (0x44 char 'D') => 27 (0x1B)
+    28, // input 69 (0x45 char 'E') => 28 (0x1C)
+    29, // input 70 (0x46 char 'F') => 29 (0x1D)
+    30, // input 71 (0x47 char 'G') => 30 (0x1E)
+    31, // input 72 (0x48 char 'H') => 31 (0x1F)
+    32, // input 73 (0x49 char 'I') => 32 (0x20)
+    33, // input 74 (0x4A char 'J') => 33 (0x21)
+    34, // input 75 (0x4B char 'K') => 34 (0x22)
+    35, // input 76 (0x4C char 'L') => 35 (0x23)
+    36, // input 77 (0x4D char 'M') => 36 (0x24)
+    37, // input 78 (0x4E char 'N') => 37 (0x25)
+    INVALID_VALUE, // input 79 (0x4F)
+    38, // input 80 (0x50 char 'P') => 38 (0x26)
+    39, // input 81 (0x51 char 'Q') => 39 (0x27)
+    40, // input 82 (0x52 char 'R') => 40 (0x28)
+    41, // input 83 (0x53 char 'S') => 41 (0x29)
+    42, // input 84 (0x54 char 'T') => 42 (0x2A)
+    43, // input 85 (0x55 char 'U') => 43 (0x2B)
+    44, // input 86 (0x56 char 'V') => 44 (0x2C)
+    INVALID_VALUE, // input 87 (0x57)
+    45, // input 88 (0x58 char 'X') => 45 (0x2D)
+    46, // input 89 (0x59 char 'Y') => 46 (0x2E)
+    47, // input 90 (0x5A char 'Z') => 47 (0x2F)
+    48, // input 91 (0x5B char '[') => 48 (0x30)
+    INVALID_VALUE, // input 92 (0x5C)
+    INVALID_VALUE, // input 93 (0x5D)
+    INVALID_VALUE, // input 94 (0x5E)
+    INVALID_VALUE, // input 95 (0x5F)
+    49, // input 96 (0x60 char '`') => 49 (0x31)
+    50, // input 97 (0x61 char 'a') => 50 (0x32)
+    51, // input 98 (0x62 char 'b') => 51 (0x33)
+    52, // input 99 (0x63 char 'c') => 52 (0x34)
+    53, // input 100 (0x64 char 'd') => 53 (0x35)
+    54, // input 101 (0x65 char 'e') => 54 (0x36)
+    INVALID_VALUE, // input 102 (0x66)
+    INVALID_VALUE, // input 103 (0x67)
+    55, // input 104 (0x68 char 'h') => 55 (0x37)
+    56, // input 105 (0x69 char 'i') => 56 (0x38)
+    57, // input 106 (0x6A char 'j') => 57 (0x39)
+    58, // input 107 (0x6B char 'k') => 58 (0x3A)
+    59, // input 108 (0x6C char 'l') => 59 (0x3B)
+    60, // input 109 (0x6D char 'm') => 60 (0x3C)
+    INVALID_VALUE, // input 110 (0x6E)
+    INVALID_VALUE, // input 111 (0x6F)
+    61, // input 112 (0x70 char 'p') => 61 (0x3D)
+    62, // input 113 (0x71 char 'q') => 62 (0x3E)
+    63, // input 114 (0x72 char 'r') => 63 (0x3F)
+    INVALID_VALUE, // input 115 (0x73)
+    INVALID_VALUE, // input 116 (0x74)
+    INVALID_VALUE, // input 117 (0x75)
+    INVALID_VALUE, // input 118 (0x76)
+    INVALID_VALUE, // input 119 (0x77)
+    INVALID_VALUE, // input 120 (0x78)
+    INVALID_VALUE, // input 121 (0x79)
+    INVALID_VALUE, // input 122 (0x7A)
+    INVALID_VALUE, // input 123 (0x7B)
+    INVALID_VALUE, // input 124 (0x7C)
+    INVALID_VALUE, // input 125 (0x7D)
+    INVALID_VALUE, // input 126 (0x7E)
+    INVALID_VALUE, // input 127 (0x7F)
+    INVALID_VALUE, // input 128 (0x80)
+    INVALID_VALUE, // input 129 (0x81)
+    INVALID_VALUE, // input 130 (0x82)
+    INVALID_VALUE, // input 131 (0x83)
+    INVALID_VALUE, // input 132 (0x84)
+    INVALID_VALUE, // input 133 (0x85)
+    INVALID_VALUE, // input 134 (0x86)
+    INVALID_VALUE, // input 135 (0x87)
+    INVALID_VALUE, // input 136 (0x88)
+    INVALID_VALUE, // input 137 (0x89)
+    INVALID_VALUE, // input 138 (0x8A)
+    INVALID_VALUE, // input 139 (0x8B)
+    INVALID_VALUE, // input 140 (0x8C)
+    INVALID_VALUE, // input 141 (0x8D)
+    INVALID_VALUE, // input 142 (0x8E)
+    INVALID_VALUE, // input 143 (0x8F)
+    INVALID_VALUE, // input 144 (0x90)
+    INVALID_VALUE, // input 145 (0x91)
+    INVALID_VALUE, // input 146 (0x92)
+    INVALID_VALUE, // input 147 (0x93)
+    INVALID_VALUE, // input 148 (0x94)
+    INVALID_VALUE, // input 149 (0x95)
+    INVALID_VALUE, // input 150 (0x96)
+    INVALID_VALUE, // input 151 (0x97)
+    INVALID_VALUE, // input 152 (0x98)
+    INVALID_VALUE, // input 153 (0x99)
+    INVALID_VALUE, // input 154 (0x9A)
+    INVALID_VALUE, // input 155 (0x9B)
+    INVALID_VALUE, // input 156 (0x9C)
+    INVALID_VALUE, // input 157 (0x9D)
+    INVALID_VALUE, // input 158 (0x9E)
+    INVALID_VALUE, // input 159 (0x9F)
+    INVALID_VALUE, // input 160 (0xA0)
+    INVALID_VALUE, // input 161 (0xA1)
+    INVALID_VALUE, // input 162 (0xA2)
+    INVALID_VALUE, // input 163 (0xA3)
+    INVALID_VALUE, // input 164 (0xA4)
+    INVALID_VALUE, // input 165 (0xA5)
+    INVALID_VALUE, // input 166 (0xA6)
+    INVALID_VALUE, // input 167 (0xA7)
+    INVALID_VALUE, // input 168 (0xA8)
+    INVALID_VALUE, // input 169 (0xA9)
+    INVALID_VALUE, // input 170 (0xAA)
+    INVALID_VALUE, // input 171 (0xAB)
+    INVALID_VALUE, // input 172 (0xAC)
+    INVALID_VALUE, // input 173 (0xAD)
+    INVALID_VALUE, // input 174 (0xAE)
+    INVALID_VALUE, // input 175 (0xAF)
+    INVALID_VALUE, // input 176 (0xB0)
+    INVALID_VALUE, // input 177 (0xB1)
+    INVALID_VALUE, // input 178 (0xB2)
+    INVALID_VALUE, // input 179 (0xB3)
+    INVALID_VALUE, // input 180 (0xB4)
+    INVALID_VALUE, // input 181 (0xB5)
+    INVALID_VALUE, // input 182 (0xB6)
+    INVALID_VALUE, // input 183 (0xB7)
+    INVALID_VALUE, // input 184 (0xB8)
+    INVALID_VALUE, // input 185 (0xB9)
+    INVALID_VALUE, // input 186 (0xBA)
+    INVALID_VALUE, // input 187 (0xBB)
+    INVALID_VALUE, // input 188 (0xBC)
+    INVALID_VALUE, // input 189 (0xBD)
+    INVALID_VALUE, // input 190 (0xBE)
+    INVALID_VALUE, // input 191 (0xBF)
+    INVALID_VALUE, // input 192 (0xC0)
+    INVALID_VALUE, // input 193 (0xC1)
+    INVALID_VALUE, // input 194 (0xC2)
+    INVALID_VALUE, // input 195 (0xC3)
+    INVALID_VALUE, // input 196 (0xC4)
+    INVALID_VALUE, // input 197 (0xC5)
+    INVALID_VALUE, // input 198 (0xC6)
+    INVALID_VALUE, // input 199 (0xC7)
+    INVALID_VALUE, // input 200 (0xC8)
+    INVALID_VALUE, // input 201 (0xC9)
+    INVALID_VALUE, // input 202 (0xCA)
+    INVALID_VALUE, // input 203 (0xCB)
+    INVALID_VALUE, // input 204 (0xCC)
+    INVALID_VALUE, // input 205 (0xCD)
+    INVALID_VALUE, // input 206 (0xCE)
+    INVALID_VALUE, // input 207 (0xCF)
+    INVALID_VALUE, // input 208 (0xD0)
+    INVALID_VALUE, // input 209 (0xD1)
+    INVALID_VALUE, // input 210 (0xD2)
+    INVALID_VALUE, // input 211 (0xD3)
+    INVALID_VALUE, // input 212 (0xD4)
+    INVALID_VALUE, // input 213 (0xD5)
+    INVALID_VALUE, // input 214 (0xD6)
+    INVALID_VALUE, // input 215 (0xD7)
+    INVALID_VALUE, // input 216 (0xD8)
+    INVALID_VALUE, // input 217 (0xD9)
+    INVALID_VALUE, // input 218 (0xDA)
+    INVALID_VALUE, // input 219 (0xDB)
+    INVALID_VALUE, // input 220 (0xDC)
+    INVALID_VALUE, // input 221 (0xDD)
+    INVALID_VALUE, // input 222 (0xDE)
+    INVALID_VALUE, // input 223 (0xDF)
+    INVALID_VALUE, // input 224 (0xE0)
+    INVALID_VALUE, // input 225 (0xE1)
+    INVALID_VALUE, // input 226 (0xE2)
+    INVALID_VALUE, // input 227 (0xE3)
+    INVALID_VALUE, // input 228 (0xE4)
+    INVALID_VALUE, // input 229 (0xE5)
+    INVALID_VALUE, // input 230 (0xE6)
+    INVALID_VALUE, // input 231 (0xE7)
+    INVALID_VALUE, // input 232 (0xE8)
+    INVALID_VALUE, // input 233 (0xE9)
+    INVALID_VALUE, // input 234 (0xEA)
+    INVALID_VALUE, // input 235 (0xEB)
+    INVALID_VALUE, // input 236 (0xEC)
+    INVALID_VALUE, // input 237 (0xED)
+    INVALID_VALUE, // input 238 (0xEE)
+    INVALID_VALUE, // input 239 (0xEF)
+    INVALID_VALUE, // input 240 (0xF0)
+    INVALID_VALUE, // input 241 (0xF1)
+    INVALID_VALUE, // input 242 (0xF2)
+    INVALID_VALUE, // input 243 (0xF3)
+    INVALID_VALUE, // input 244 (0xF4)
+    INVALID_VALUE, // input 245 (0xF5)
+    INVALID_VALUE, // input 246 (0xF6)
+    INVALID_VALUE, // input 247 (0xF7)
+    INVALID_VALUE, // input 248 (0xF8)
+    INVALID_VALUE, // input 249 (0xF9)
+    INVALID_VALUE, // input 250 (0xFA)
+    INVALID_VALUE, // input 251 (0xFB)
+    INVALID_VALUE, // input 252 (0xFC)
+    INVALID_VALUE, // input 253 (0xFD)
+    INVALID_VALUE, // input 254 (0xFE)
+    INVALID_VALUE, // input 255 (0xFF)
+];
diff --git a/src/tests.rs b/src/tests.rs
new file mode 100644
index 0000000..88748de
--- /dev/null
+++ b/src/tests.rs
@@ -0,0 +1,81 @@
+use crate::{decode_config, encode::encoded_size, encode_config_buf, CharacterSet, Config};
+
+use std::str;
+
+use rand::{
+    distributions::{Distribution, Uniform},
+    seq::SliceRandom,
+    FromEntropy, Rng,
+};
+
+#[test]
+fn roundtrip_random_config_short() {
+    // exercise the slower encode/decode routines that operate on shorter buffers more vigorously
+    roundtrip_random_config(Uniform::new(0, 50), 10_000);
+}
+
+#[test]
+fn roundtrip_random_config_long() {
+    roundtrip_random_config(Uniform::new(0, 1000), 10_000);
+}
+
+pub fn assert_encode_sanity(encoded: &str, config: Config, input_len: usize) {
+    let input_rem = input_len % 3;
+    let expected_padding_len = if input_rem > 0 {
+        if config.pad {
+            3 - input_rem
+        } else {
+            0
+        }
+    } else {
+        0
+    };
+
+    let expected_encoded_len = encoded_size(input_len, config).unwrap();
+
+    assert_eq!(expected_encoded_len, encoded.len());
+
+    let padding_len = encoded.chars().filter(|&c| c == '=').count();
+
+    assert_eq!(expected_padding_len, padding_len);
+
+    let _ = str::from_utf8(encoded.as_bytes()).expect("Base64 should be valid utf8");
+}
+
+fn roundtrip_random_config(input_len_range: Uniform<usize>, iterations: u32) {
+    let mut input_buf: Vec<u8> = Vec::new();
+    let mut encoded_buf = String::new();
+    let mut rng = rand::rngs::SmallRng::from_entropy();
+
+    for _ in 0..iterations {
+        input_buf.clear();
+        encoded_buf.clear();
+
+        let input_len = input_len_range.sample(&mut rng);
+
+        let config = random_config(&mut rng);
+
+        for _ in 0..input_len {
+            input_buf.push(rng.gen());
+        }
+
+        encode_config_buf(&input_buf, config, &mut encoded_buf);
+
+        assert_encode_sanity(&encoded_buf, config, input_len);
+
+        assert_eq!(input_buf, decode_config(&encoded_buf, config).unwrap());
+    }
+}
+
+pub fn random_config<R: Rng>(rng: &mut R) -> Config {
+    const CHARSETS: &[CharacterSet] = &[
+        CharacterSet::UrlSafe,
+        CharacterSet::Standard,
+        CharacterSet::Crypt,
+        CharacterSet::ImapMutf7,
+        CharacterSet::BinHex,
+    ];
+    let charset = *CHARSETS.choose(rng).unwrap();
+
+    Config::new(charset, rng.gen())
+}
diff --git a/src/write/encoder.rs b/src/write/encoder.rs
new file mode 100644
index 0000000..8a48f43
--- /dev/null
+++ b/src/write/encoder.rs
@@ -0,0 +1,381 @@
+use crate::encode::encode_to_slice;
+use crate::{encode_config_slice, Config};
+use std::{
+    cmp, fmt,
+    io::{ErrorKind, Result, Write},
+};
+
+pub(crate) const BUF_SIZE: usize = 1024;
+/// The most bytes whose encoding will fit in `BUF_SIZE`
+const MAX_INPUT_LEN: usize = BUF_SIZE / 4 * 3;
+// 3 bytes of input = 4 bytes of base64, always (because we don't allow line wrapping)
+const MIN_ENCODE_CHUNK_SIZE: usize = 3;
+
+/// A `Write` implementation that base64 encodes data before delegating to the wrapped writer.
+///
+/// Because base64 has special handling for the end of the input data (padding, etc), there's a
+/// `finish()` method on this type that encodes any leftover input bytes and adds padding if
+/// appropriate. It's called automatically when deallocated (see the `Drop` implementation), but
+/// any error that occurs when invoking the underlying writer will be suppressed. If you want to
+/// handle such errors, call `finish()` yourself.
+///
+/// # Examples
+///
+/// ```
+/// use std::io::Write;
+///
+/// // use a vec as the simplest possible `Write` -- in real code this is probably a file, etc.
+/// let mut enc = base64::write::EncoderWriter::new(Vec::new(), base64::STANDARD);
+///
+/// // handle errors as you normally would
+/// enc.write_all(b"asdf").unwrap();
+///
+/// // could leave this out to be called by Drop, if you don't care
+/// // about handling errors or getting the delegate writer back
+/// let delegate = enc.finish().unwrap();
+///
+/// // base64 was written to the writer
+/// assert_eq!(b"YXNkZg==", &delegate[..]);
+///
+/// ```
+///
+/// # Panics
+///
+/// Calling `write()` (or related methods) or `finish()` after `finish()` has completed without
+/// error is invalid and will panic.
+///
+/// # Errors
+///
+/// Base64 encoding itself does not generate errors, but errors from the wrapped writer will be
+/// returned as per the contract of `Write`.
+///
+/// # Performance
+///
+/// It has some minor performance loss compared to encoding slices (a couple percent).
+/// It does not do any heap allocation.
+pub struct EncoderWriter<W: Write> {
+    config: Config,
+    /// Where encoded data is written to. It's an Option as it's None immediately before Drop is
+    /// called so that finish() can return the underlying writer. None implies that finish() has
+    /// been called successfully.
+    delegate: Option<W>,
+    /// Holds a partial chunk, if any, after the last `write()`, so that we may then fill the chunk
+    /// with the next `write()`, encode it, then proceed with the rest of the input normally.
+    extra_input: [u8; MIN_ENCODE_CHUNK_SIZE],
+    /// How much of `extra` is occupied, in `[0, MIN_ENCODE_CHUNK_SIZE]`.
+    extra_input_occupied_len: usize,
+    /// Buffer to encode into. May hold leftover encoded bytes from a previous write call that the underlying writer
+    /// did not write last time.
+    output: [u8; BUF_SIZE],
+    /// How much of `output` is occupied with encoded data that couldn't be written last time
+    output_occupied_len: usize,
+    /// panic safety: don't write again in destructor if writer panicked while we were writing to it
+    panicked: bool,
+}
+
+impl<W: Write> fmt::Debug for EncoderWriter<W> {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(
+            f,
+            "extra_input: {:?} extra_input_occupied_len:{:?} output[..5]: {:?} output_occupied_len: {:?}",
+            self.extra_input,
+            self.extra_input_occupied_len,
+            &self.output[0..5],
+            self.output_occupied_len
+        )
+    }
+}
+
+impl<W: Write> EncoderWriter<W> {
+    /// Create a new encoder that will write to the provided delegate writer `w`.
+    pub fn new(w: W, config: Config) -> EncoderWriter<W> {
+        EncoderWriter {
+            config,
+            delegate: Some(w),
+            extra_input: [0u8; MIN_ENCODE_CHUNK_SIZE],
+            extra_input_occupied_len: 0,
+            output: [0u8; BUF_SIZE],
+            output_occupied_len: 0,
+            panicked: false,
+        }
+    }
+
+    /// Encode all remaining buffered data and write it, including any trailing incomplete input
+    /// triples and associated padding.
+    ///
+    /// Once this succeeds, no further writes or calls to this method are allowed.
+    ///
+    /// This may write to the delegate writer multiple times if the delegate writer does not accept
+    /// all input provided to its `write` each invocation.
+    ///
+    /// If you don't care about error handling, it is not necessary to call this function, as the
+    /// equivalent finalization is done by the Drop impl.
+    ///
+    /// Returns the writer that this was constructed around.
+    ///
+    /// # Errors
+    ///
+    /// The first error that is not of `ErrorKind::Interrupted` will be returned.
+    pub fn finish(&mut self) -> Result<W> {
+        // If we could consume self in finish(), we wouldn't have to worry about this case, but
+        // finish() is retryable in the face of I/O errors, so we can't consume here.
+        if self.delegate.is_none() {
+            panic!("Encoder has already had finish() called")
+        };
+
+        self.write_final_leftovers()?;
+
+        let writer = self.delegate.take().expect("Writer must be present");
+
+        Ok(writer)
+    }
+
+    /// Write any remaining buffered data to the delegate writer.
+    fn write_final_leftovers(&mut self) -> Result<()> {
+        if self.delegate.is_none() {
+            // finish() has already successfully called this, and we are now in drop() with a None
+            // writer, so just no-op
+            return Ok(());
+        }
+
+        self.write_all_encoded_output()?;
+
+        if self.extra_input_occupied_len > 0 {
+            let encoded_len = encode_config_slice(
+                &self.extra_input[..self.extra_input_occupied_len],
+                self.config,
+                &mut self.output[..],
+            );
+
+            self.output_occupied_len = encoded_len;
+
+            self.write_all_encoded_output()?;
+
+            // write succeeded, do not write the encoding of extra again if finish() is retried
+            self.extra_input_occupied_len = 0;
+        }
+
+        Ok(())
+    }
+
+    /// Write as much of the encoded output to the delegate writer as it will accept, and store the
+    /// leftovers to be attempted at the next write() call. Updates `self.output_occupied_len`.
+    ///
+    /// # Errors
+    ///
+    /// Errors from the delegate writer are returned. In the case of an error,
+    /// `self.output_occupied_len` will not be updated, as errors from `write` are specified to mean
+    /// that no write took place.
+    fn write_to_delegate(&mut self, current_output_len: usize) -> Result<()> {
+        self.panicked = true;
+        let res = self
+            .delegate
+            .as_mut()
+            .expect("Writer must be present")
+            .write(&self.output[..current_output_len]);
+        self.panicked = false;
+
+        res.map(|consumed| {
+            debug_assert!(consumed <= current_output_len);
+
+            if consumed < current_output_len {
+                self.output_occupied_len = current_output_len.checked_sub(consumed).unwrap();
+                // If we're blocking on I/O, the minor inefficiency of copying bytes to the
+                // start of the buffer is the least of our concerns...
+                // Rotate moves more than we need to, but copy_within isn't stabilized yet.
+                self.output.rotate_left(consumed);
+            } else {
+                self.output_occupied_len = 0;
+            }
+        })
+    }
+
+    /// Write all buffered encoded output. If this returns `Ok`, `self.output_occupied_len` is `0`.
+    ///
+    /// This is basically write_all for the remaining buffered data but without the undesirable
+    /// abort-on-`Ok(0)` behavior.
+    ///
+    /// # Errors
+    ///
+    /// Any error emitted by the delegate writer abort the write loop and is returned, unless it's
+    /// `Interrupted`, in which case the error is ignored and writes will continue.
+    fn write_all_encoded_output(&mut self) -> Result<()> {
+        while self.output_occupied_len > 0 {
+            let remaining_len = self.output_occupied_len;
+            match self.write_to_delegate(remaining_len) {
+                // try again on interrupts ala write_all
+                Err(ref e) if e.kind() == ErrorKind::Interrupted => {}
+                // other errors return
+                Err(e) => return Err(e),
+                // success no-ops because remaining length is already updated
+                Ok(_) => {}
+            };
+        }
+
+        debug_assert_eq!(0, self.output_occupied_len);
+        Ok(())
+    }
+}
+
+impl<W: Write> Write for EncoderWriter<W> {
+    /// Encode input and then write to the delegate writer.
+    ///
+    /// Under non-error circumstances, this returns `Ok` with the value being the number of bytes
+    /// of `input` consumed. The value may be `0`, which interacts poorly with `write_all`, which
+    /// interprets `Ok(0)` as an error, despite it being allowed by the contract of `write`. See
+    /// https://github.com/rust-lang/rust/issues/56889 for more on that.
+    ///
+    /// If the previous call to `write` provided more (encoded) data than the delegate writer could
+    /// accept in a single call to its `write`, the remaining data is buffered. As long as buffered
+    /// data is present, subsequent calls to `write` will try to write the remaining buffered data
+    /// to the delegate and return either `Ok(0)` -- and therefore not consume any of `input` -- or
+    /// an error.
+    ///
+    /// # Errors
+    ///
+    /// Any errors emitted by the delegate writer are returned.
+    fn write(&mut self, input: &[u8]) -> Result<usize> {
+        if self.delegate.is_none() {
+            panic!("Cannot write more after calling finish()");
+        }
+
+        if input.is_empty() {
+            return Ok(0);
+        }
+
+        // The contract of `Write::write` places some constraints on this implementation:
+        // - a call to `write()` represents at most one call to a wrapped `Write`, so we can't
+        // iterate over the input and encode multiple chunks.
+        // - Errors mean that "no bytes were written to this writer", so we need to reset the
+        // internal state to what it was before the error occurred
+
+        // before reading any input, write any leftover encoded output from last time
+        if self.output_occupied_len > 0 {
+            let current_len = self.output_occupied_len;
+            return self
+                .write_to_delegate(current_len)
+                // did not read any input
+                .map(|_| 0);
+        }
+
+        debug_assert_eq!(0, self.output_occupied_len);
+
+        // how many bytes, if any, were read into `extra` to create a triple to encode
+        let mut extra_input_read_len = 0;
+        let mut input = input;
+
+        let orig_extra_len = self.extra_input_occupied_len;
+
+        let mut encoded_size = 0;
+        // always a multiple of MIN_ENCODE_CHUNK_SIZE
+        let mut max_input_len = MAX_INPUT_LEN;
+
+        // process leftover un-encoded input from last write
+        if self.extra_input_occupied_len > 0 {
+            debug_assert!(self.extra_input_occupied_len < 3);
+            if input.len() + self.extra_input_occupied_len >= MIN_ENCODE_CHUNK_SIZE {
+                // Fill up `extra`, encode that into `output`, and consume as much of the rest of
+                // `input` as possible.
+                // We could write just the encoding of `extra` by itself but then we'd have to
+                // return after writing only 4 bytes, which is inefficient if the underlying writer
+                // would make a syscall.
+                extra_input_read_len = MIN_ENCODE_CHUNK_SIZE - self.extra_input_occupied_len;
+                debug_assert!(extra_input_read_len > 0);
+                // overwrite only bytes that weren't already used. If we need to rollback extra_len
+                // (when the subsequent write errors), the old leading bytes will still be there.
+                self.extra_input[self.extra_input_occupied_len..MIN_ENCODE_CHUNK_SIZE]
+                    .copy_from_slice(&input[0..extra_input_read_len]);
+
+                let len = encode_to_slice(
+                    &self.extra_input[0..MIN_ENCODE_CHUNK_SIZE],
+                    &mut self.output[..],
+                    self.config.char_set.encode_table(),
+                );
+                debug_assert_eq!(4, len);
+
+                input = &input[extra_input_read_len..];
+
+                // consider extra to be used up, since we encoded it
+                self.extra_input_occupied_len = 0;
+                // don't clobber where we just encoded to
+                encoded_size = 4;
+                // and don't read more than can be encoded
+                max_input_len = MAX_INPUT_LEN - MIN_ENCODE_CHUNK_SIZE;
+
+            // fall through to normal encoding
+            } else {
+                // `extra` and `input` are non empty, but `|extra| + |input| < 3`, so there must be
+                // 1 byte in each.
+                debug_assert_eq!(1, input.len());
+                debug_assert_eq!(1, self.extra_input_occupied_len);
+
+                self.extra_input[self.extra_input_occupied_len] = input[0];
+                self.extra_input_occupied_len += 1;
+                return Ok(1);
+            };
+        } else if input.len() < MIN_ENCODE_CHUNK_SIZE {
+            // `extra` is empty, and `input` fits inside it
+            self.extra_input[0..input.len()].copy_from_slice(input);
+            self.extra_input_occupied_len = input.len();
+            return Ok(input.len());
+        };
+
+        // either 0 or 1 complete chunks encoded from extra
+        debug_assert!(encoded_size == 0 || encoded_size == 4);
+        debug_assert!(
+            // didn't encode extra input
+            MAX_INPUT_LEN == max_input_len
+                // encoded one triple
+                || MAX_INPUT_LEN == max_input_len + MIN_ENCODE_CHUNK_SIZE
+        );
+
+        // encode complete triples only
+        let input_complete_chunks_len = input.len() - (input.len() % MIN_ENCODE_CHUNK_SIZE);
+        let input_chunks_to_encode_len = cmp::min(input_complete_chunks_len, max_input_len);
+        debug_assert_eq!(0, max_input_len % MIN_ENCODE_CHUNK_SIZE);
+        debug_assert_eq!(0, input_chunks_to_encode_len % MIN_ENCODE_CHUNK_SIZE);
+
+        encoded_size += encode_to_slice(
+            &input[..(input_chunks_to_encode_len)],
+            &mut self.output[encoded_size..],
+            self.config.char_set.encode_table(),
+        );
+
+        // not updating `self.output_occupied_len` here because if the below write fails, it should
+        // "never take place" -- the buffer contents we encoded are ignored and perhaps retried
+        // later, if the consumer chooses.
+
+        self.write_to_delegate(encoded_size)
+            // no matter whether we wrote the full encoded buffer or not, we consumed the same
+            // input
+            .map(|_| extra_input_read_len + input_chunks_to_encode_len)
+            .map_err(|e| {
+                // in case we filled and encoded `extra`, reset extra_len
+                self.extra_input_occupied_len = orig_extra_len;
+
+                e
+            })
+    }
+
+    /// Because this is usually treated as OK to call multiple times, it will *not* flush any
+    /// incomplete chunks of input or write padding.
+    /// # Errors
+    ///
+    /// The first error that is not of [`ErrorKind::Interrupted`] will be returned.
+    fn flush(&mut self) -> Result<()> {
+        self.write_all_encoded_output()?;
+        self.delegate
+            .as_mut()
+            .expect("Writer must be present")
+            .flush()
+    }
+}
+
+impl<W: Write> Drop for EncoderWriter<W> {
+    fn drop(&mut self) {
+        if !self.panicked {
+            // like `BufWriter`, ignore errors during drop
+            let _ = self.write_final_leftovers();
+        }
+    }
+}
diff --git a/src/write/encoder_string_writer.rs b/src/write/encoder_string_writer.rs
new file mode 100644
index 0000000..58b1c0a
--- /dev/null
+++ b/src/write/encoder_string_writer.rs
@@ -0,0 +1,176 @@
+use super::encoder::EncoderWriter;
+use crate::Config;
+use std::io;
+use std::io::Write;
+
+/// A `Write` implementation that base64-encodes data using the provided config and accumulates the
+/// resulting base64 in memory, which is then exposed as a String via `into_inner()`.
+///
+/// # Examples
+///
+/// Buffer base64 in a new String:
+///
+/// ```
+/// use std::io::Write;
+///
+/// let mut enc = base64::write::EncoderStringWriter::new(base64::STANDARD);
+///
+/// enc.write_all(b"asdf").unwrap();
+///
+/// // get the resulting String
+/// let b64_string = enc.into_inner();
+///
+/// assert_eq!("YXNkZg==", &b64_string);
+/// ```
+///
+/// Or, append to an existing String:
+///
+/// ```
+/// use std::io::Write;
+///
+/// let mut buf = String::from("base64: ");
+///
+/// let mut enc = base64::write::EncoderStringWriter::from(&mut buf, base64::STANDARD);
+///
+/// enc.write_all(b"asdf").unwrap();
+///
+/// // release the &mut reference on buf
+/// let _ = enc.into_inner();
+///
+/// assert_eq!("base64: YXNkZg==", &buf);
+/// ```
+///
+/// # Panics
+///
+/// Calling `write()` (or related methods) or `finish()` after `finish()` has completed without
+/// error is invalid and will panic.
+///
+/// # Performance
+///
+/// Because it has to validate that the base64 is UTF-8, it is about 80% as fast as writing plain
+/// bytes to a `io::Write`.
+pub struct EncoderStringWriter<S: StrConsumer> {
+    encoder: EncoderWriter<Utf8SingleCodeUnitWriter<S>>,
+}
+
+impl<S: StrConsumer> EncoderStringWriter<S> {
+    /// Create a EncoderStringWriter that will append to the provided `StrConsumer`.
+    pub fn from(str_consumer: S, config: Config) -> Self {
+        EncoderStringWriter {
+            encoder: EncoderWriter::new(Utf8SingleCodeUnitWriter { str_consumer }, config),
+        }
+    }
+
+    /// Encode all remaining buffered data, including any trailing incomplete input triples and
+    /// associated padding.
+    ///
+    /// Once this succeeds, no further writes or calls to this method are allowed.
+    ///
+    /// Returns the base64-encoded form of the accumulated written data.
+    pub fn into_inner(mut self) -> S {
+        self.encoder
+            .finish()
+            .expect("Writing to a Vec<u8> should never fail")
+            .str_consumer
+    }
+}
+
+impl EncoderStringWriter<String> {
+    /// Create a EncoderStringWriter that will encode into a new String with the provided config.
+    pub fn new(config: Config) -> Self {
+        EncoderStringWriter::from(String::new(), config)
+    }
+}
+
+impl<S: StrConsumer> Write for EncoderStringWriter<S> {
+    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+        self.encoder.write(buf)
+    }
+
+    fn flush(&mut self) -> io::Result<()> {
+        self.encoder.flush()
+    }
+}
+
+/// An abstraction around consuming `str`s produced by base64 encoding.
+pub trait StrConsumer {
+    /// Consume the base64 encoded data in `buf`
+    fn consume(&mut self, buf: &str);
+}
+
+/// As for io::Write, `StrConsumer` is implemented automatically for `&mut S`.
+impl<S: StrConsumer + ?Sized> StrConsumer for &mut S {
+    fn consume(&mut self, buf: &str) {
+        (**self).consume(buf)
+    }
+}
+
+/// Pushes the str onto the end of the String
+impl StrConsumer for String {
+    fn consume(&mut self, buf: &str) {
+        self.push_str(buf)
+    }
+}
+
+/// A `Write` that only can handle bytes that are valid single-byte UTF-8 code units.
+///
+/// This is safe because we only use it when writing base64, which is always valid UTF-8.
+struct Utf8SingleCodeUnitWriter<S: StrConsumer> {
+    str_consumer: S,
+}
+
+impl<S: StrConsumer> io::Write for Utf8SingleCodeUnitWriter<S> {
+    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+        // Because we expect all input to be valid utf-8 individual bytes, we can encode any buffer
+        // length
+        let s = std::str::from_utf8(buf).expect("Input must be valid UTF-8");
+
+        self.str_consumer.consume(s);
+
+        Ok(buf.len())
+    }
+
+    fn flush(&mut self) -> io::Result<()> {
+        // no op
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::encode_config_buf;
+    use crate::tests::random_config;
+    use crate::write::encoder_string_writer::EncoderStringWriter;
+    use rand::Rng;
+    use std::io::Write;
+
+    #[test]
+    fn every_possible_split_of_input() {
+        let mut rng = rand::thread_rng();
+        let mut orig_data = Vec::<u8>::new();
+        let mut normal_encoded = String::new();
+
+        let size = 5_000;
+
+        for i in 0..size {
+            orig_data.clear();
+            normal_encoded.clear();
+
+            for _ in 0..size {
+                orig_data.push(rng.gen());
+            }
+
+            let config = random_config(&mut rng);
+            encode_config_buf(&orig_data, config, &mut normal_encoded);
+
+            let mut stream_encoder = EncoderStringWriter::new(config);
+            // Write the first i bytes, then the rest
+            stream_encoder.write_all(&orig_data[0..i]).unwrap();
+            stream_encoder.write_all(&orig_data[i..]).unwrap();
+
+            let stream_encoded = stream_encoder.into_inner();
+
+            assert_eq!(normal_encoded, stream_encoded);
+        }
+    }
+}
diff --git a/src/write/encoder_tests.rs b/src/write/encoder_tests.rs
new file mode 100644
index 0000000..09b4d3a
--- /dev/null
+++ b/src/write/encoder_tests.rs
@@ -0,0 +1,568 @@
+use super::EncoderWriter;
+use crate::tests::random_config;
+use crate::{encode_config, encode_config_buf, STANDARD_NO_PAD, URL_SAFE};
+
+use std::io::{Cursor, Write};
+use std::{cmp, io, str};
+
+use rand::Rng;
+
+#[test]
+fn encode_three_bytes() {
+    let mut c = Cursor::new(Vec::new());
+    {
+        let mut enc = EncoderWriter::new(&mut c, URL_SAFE);
+
+        let sz = enc.write(b"abc").unwrap();
+        assert_eq!(sz, 3);
+    }
+    assert_eq!(&c.get_ref()[..], encode_config("abc", URL_SAFE).as_bytes());
+}
+
+#[test]
+fn encode_nine_bytes_two_writes() {
+    let mut c = Cursor::new(Vec::new());
+    {
+        let mut enc = EncoderWriter::new(&mut c, URL_SAFE);
+
+        let sz = enc.write(b"abcdef").unwrap();
+        assert_eq!(sz, 6);
+        let sz = enc.write(b"ghi").unwrap();
+        assert_eq!(sz, 3);
+    }
+    assert_eq!(
+        &c.get_ref()[..],
+        encode_config("abcdefghi", URL_SAFE).as_bytes()
+    );
+}
+
+#[test]
+fn encode_one_then_two_bytes() {
+    let mut c = Cursor::new(Vec::new());
+    {
+        let mut enc = EncoderWriter::new(&mut c, URL_SAFE);
+
+        let sz = enc.write(b"a").unwrap();
+        assert_eq!(sz, 1);
+        let sz = enc.write(b"bc").unwrap();
+        assert_eq!(sz, 2);
+    }
+    assert_eq!(&c.get_ref()[..], encode_config("abc", URL_SAFE).as_bytes());
+}
+
+#[test]
+fn encode_one_then_five_bytes() {
+    let mut c = Cursor::new(Vec::new());
+    {
+        let mut enc = EncoderWriter::new(&mut c, URL_SAFE);
+
+        let sz = enc.write(b"a").unwrap();
+        assert_eq!(sz, 1);
+        let sz = enc.write(b"bcdef").unwrap();
+        assert_eq!(sz, 5);
+    }
+    assert_eq!(
+        &c.get_ref()[..],
+        encode_config("abcdef", URL_SAFE).as_bytes()
+    );
+}
+
+#[test]
+fn encode_1_2_3_bytes() {
+    let mut c = Cursor::new(Vec::new());
+    {
+        let mut enc = EncoderWriter::new(&mut c, URL_SAFE);
+
+        let sz = enc.write(b"a").unwrap();
+        assert_eq!(sz, 1);
+        let sz = enc.write(b"bc").unwrap();
+        assert_eq!(sz, 2);
+        let sz = enc.write(b"def").unwrap();
+        assert_eq!(sz, 3);
+    }
+    assert_eq!(
+        &c.get_ref()[..],
+        encode_config("abcdef", URL_SAFE).as_bytes()
+    );
+}
+
+#[test]
+fn encode_with_padding() {
+    let mut c = Cursor::new(Vec::new());
+    {
+        let mut enc = EncoderWriter::new(&mut c, URL_SAFE);
+
+        enc.write_all(b"abcd").unwrap();
+
+        enc.flush().unwrap();
+    }
+    assert_eq!(&c.get_ref()[..], encode_config("abcd", URL_SAFE).as_bytes());
+}
+
+#[test]
+fn encode_with_padding_multiple_writes() {
+    let mut c = Cursor::new(Vec::new());
+    {
+        let mut enc = EncoderWriter::new(&mut c, URL_SAFE);
+
+        assert_eq!(1, enc.write(b"a").unwrap());
+        assert_eq!(2, enc.write(b"bc").unwrap());
+        assert_eq!(3, enc.write(b"def").unwrap());
+        assert_eq!(1, enc.write(b"g").unwrap());
+
+        enc.flush().unwrap();
+    }
+    assert_eq!(
+        &c.get_ref()[..],
+        encode_config("abcdefg", URL_SAFE).as_bytes()
+    );
+}
+
+#[test]
+fn finish_writes_extra_byte() {
+    let mut c = Cursor::new(Vec::new());
+    {
+        let mut enc = EncoderWriter::new(&mut c, URL_SAFE);
+
+        assert_eq!(6, enc.write(b"abcdef").unwrap());
+
+        // will be in extra
+        assert_eq!(1, enc.write(b"g").unwrap());
+
+        // 1 trailing byte = 2 encoded chars
+        let _ = enc.finish().unwrap();
+    }
+    assert_eq!(
+        &c.get_ref()[..],
+        encode_config("abcdefg", URL_SAFE).as_bytes()
+    );
+}
+
+#[test]
+fn write_partial_chunk_encodes_partial_chunk() {
+    let mut c = Cursor::new(Vec::new());
+    {
+        let mut enc = EncoderWriter::new(&mut c, STANDARD_NO_PAD);
+
+        // nothing encoded yet
+        assert_eq!(2, enc.write(b"ab").unwrap());
+        // encoded here
+        let _ = enc.finish().unwrap();
+    }
+    assert_eq!(
+        &c.get_ref()[..],
+        encode_config("ab", STANDARD_NO_PAD).as_bytes()
+    );
+    assert_eq!(3, c.get_ref().len());
+}
+
+#[test]
+fn write_1_chunk_encodes_complete_chunk() {
+    let mut c = Cursor::new(Vec::new());
+    {
+        let mut enc = EncoderWriter::new(&mut c, STANDARD_NO_PAD);
+
+        assert_eq!(3, enc.write(b"abc").unwrap());
+        let _ = enc.finish().unwrap();
+    }
+    assert_eq!(
+        &c.get_ref()[..],
+        encode_config("abc", STANDARD_NO_PAD).as_bytes()
+    );
+    assert_eq!(4, c.get_ref().len());
+}
+
+#[test]
+fn write_1_chunk_and_partial_encodes_only_complete_chunk() {
+    let mut c = Cursor::new(Vec::new());
+    {
+        let mut enc = EncoderWriter::new(&mut c, STANDARD_NO_PAD);
+
+        // "d" not written
+        assert_eq!(3, enc.write(b"abcd").unwrap());
+        let _ = enc.finish().unwrap();
+    }
+    assert_eq!(
+        &c.get_ref()[..],
+        encode_config("abc", STANDARD_NO_PAD).as_bytes()
+    );
+    assert_eq!(4, c.get_ref().len());
+}
+
+#[test]
+fn write_2_partials_to_exactly_complete_chunk_encodes_complete_chunk() {
+    let mut c = Cursor::new(Vec::new());
+    {
+        let mut enc = EncoderWriter::new(&mut c, STANDARD_NO_PAD);
+
+        assert_eq!(1, enc.write(b"a").unwrap());
+        assert_eq!(2, enc.write(b"bc").unwrap());
+        let _ = enc.finish().unwrap();
+    }
+    assert_eq!(
+        &c.get_ref()[..],
+        encode_config("abc", STANDARD_NO_PAD).as_bytes()
+    );
+    assert_eq!(4, c.get_ref().len());
+}
+
+#[test]
+fn write_partial_then_enough_to_complete_chunk_but_not_complete_another_chunk_encodes_complete_chunk_without_consuming_remaining(
+) {
+    let mut c = Cursor::new(Vec::new());
+    {
+        let mut enc = EncoderWriter::new(&mut c, STANDARD_NO_PAD);
+
+        assert_eq!(1, enc.write(b"a").unwrap());
+        // doesn't consume "d"
+        assert_eq!(2, enc.write(b"bcd").unwrap());
+        let _ = enc.finish().unwrap();
+    }
+    assert_eq!(
+        &c.get_ref()[..],
+        encode_config("abc", STANDARD_NO_PAD).as_bytes()
+    );
+    assert_eq!(4, c.get_ref().len());
+}
+
+#[test]
+fn write_partial_then_enough_to_complete_chunk_and_another_chunk_encodes_complete_chunks() {
+    let mut c = Cursor::new(Vec::new());
+    {
+        let mut enc = EncoderWriter::new(&mut c, STANDARD_NO_PAD);
+
+        assert_eq!(1, enc.write(b"a").unwrap());
+        // completes partial chunk, and another chunk
+        assert_eq!(5, enc.write(b"bcdef").unwrap());
+        let _ = enc.finish().unwrap();
+    }
+    assert_eq!(
+        &c.get_ref()[..],
+        encode_config("abcdef", STANDARD_NO_PAD).as_bytes()
+    );
+    assert_eq!(8, c.get_ref().len());
+}
+
+#[test]
+fn write_partial_then_enough_to_complete_chunk_and_another_chunk_and_another_partial_chunk_encodes_only_complete_chunks(
+) {
+    let mut c = Cursor::new(Vec::new());
+    {
+        let mut enc = EncoderWriter::new(&mut c, STANDARD_NO_PAD);
+
+        assert_eq!(1, enc.write(b"a").unwrap());
+        // completes partial chunk, and another chunk, with one more partial chunk that's not
+        // consumed
+        assert_eq!(5, enc.write(b"bcdefe").unwrap());
+        let _ = enc.finish().unwrap();
+    }
+    assert_eq!(
+        &c.get_ref()[..],
+        encode_config("abcdef", STANDARD_NO_PAD).as_bytes()
+    );
+    assert_eq!(8, c.get_ref().len());
+}
+
+#[test]
+fn drop_calls_finish_for_you() {
+    let mut c = Cursor::new(Vec::new());
+    {
+        let mut enc = EncoderWriter::new(&mut c, STANDARD_NO_PAD);
+        assert_eq!(1, enc.write(b"a").unwrap());
+    }
+    assert_eq!(
+        &c.get_ref()[..],
+        encode_config("a", STANDARD_NO_PAD).as_bytes()
+    );
+    assert_eq!(2, c.get_ref().len());
+}
+
+#[test]
+fn every_possible_split_of_input() {
+    let mut rng = rand::thread_rng();
+    let mut orig_data = Vec::<u8>::new();
+    let mut stream_encoded = Vec::<u8>::new();
+    let mut normal_encoded = String::new();
+
+    let size = 5_000;
+
+    for i in 0..size {
+        orig_data.clear();
+        stream_encoded.clear();
+        normal_encoded.clear();
+
+        for _ in 0..size {
+            orig_data.push(rng.gen());
+        }
+
+        let config = random_config(&mut rng);
+        encode_config_buf(&orig_data, config, &mut normal_encoded);
+
+        {
+            let mut stream_encoder = EncoderWriter::new(&mut stream_encoded, config);
+            // Write the first i bytes, then the rest
+            stream_encoder.write_all(&orig_data[0..i]).unwrap();
+            stream_encoder.write_all(&orig_data[i..]).unwrap();
+        }
+
+        assert_eq!(normal_encoded, str::from_utf8(&stream_encoded).unwrap());
+    }
+}
+
+#[test]
+fn encode_random_config_matches_normal_encode_reasonable_input_len() {
+    // choose up to 2 * buf size, so ~half the time it'll use a full buffer
+    do_encode_random_config_matches_normal_encode(super::encoder::BUF_SIZE * 2)
+}
+
+#[test]
+fn encode_random_config_matches_normal_encode_tiny_input_len() {
+    do_encode_random_config_matches_normal_encode(10)
+}
+
+#[test]
+fn retrying_writes_that_error_with_interrupted_works() {
+    let mut rng = rand::thread_rng();
+    let mut orig_data = Vec::<u8>::new();
+    let mut stream_encoded = Vec::<u8>::new();
+    let mut normal_encoded = String::new();
+
+    for _ in 0..1_000 {
+        orig_data.clear();
+        stream_encoded.clear();
+        normal_encoded.clear();
+
+        let orig_len: usize = rng.gen_range(100, 20_000);
+        for _ in 0..orig_len {
+            orig_data.push(rng.gen());
+        }
+
+        // encode the normal way
+        let config = random_config(&mut rng);
+        encode_config_buf(&orig_data, config, &mut normal_encoded);
+
+        // encode via the stream encoder
+        {
+            let mut interrupt_rng = rand::thread_rng();
+            let mut interrupting_writer = InterruptingWriter {
+                w: &mut stream_encoded,
+                rng: &mut interrupt_rng,
+                fraction: 0.8,
+            };
+
+            let mut stream_encoder = EncoderWriter::new(&mut interrupting_writer, config);
+            let mut bytes_consumed = 0;
+            while bytes_consumed < orig_len {
+                // use short inputs since we want to use `extra` a lot as that's what needs rollback
+                // when errors occur
+                let input_len: usize = cmp::min(rng.gen_range(0, 10), orig_len - bytes_consumed);
+
+                retry_interrupted_write_all(
+                    &mut stream_encoder,
+                    &orig_data[bytes_consumed..bytes_consumed + input_len],
+                )
+                .unwrap();
+
+                bytes_consumed += input_len;
+            }
+
+            loop {
+                let res = stream_encoder.finish();
+                match res {
+                    Ok(_) => break,
+                    Err(e) => match e.kind() {
+                        io::ErrorKind::Interrupted => continue,
+                        _ => Err(e).unwrap(), // bail
+                    },
+                }
+            }
+
+            assert_eq!(orig_len, bytes_consumed);
+        }
+
+        assert_eq!(normal_encoded, str::from_utf8(&stream_encoded).unwrap());
+    }
+}
+
+#[test]
+fn writes_that_only_write_part_of_input_and_sometimes_interrupt_produce_correct_encoded_data() {
+    let mut rng = rand::thread_rng();
+    let mut orig_data = Vec::<u8>::new();
+    let mut stream_encoded = Vec::<u8>::new();
+    let mut normal_encoded = String::new();
+
+    for _ in 0..1_000 {
+        orig_data.clear();
+        stream_encoded.clear();
+        normal_encoded.clear();
+
+        let orig_len: usize = rng.gen_range(100, 20_000);
+        for _ in 0..orig_len {
+            orig_data.push(rng.gen());
+        }
+
+        // encode the normal way
+        let config = random_config(&mut rng);
+        encode_config_buf(&orig_data, config, &mut normal_encoded);
+
+        // encode via the stream encoder
+        {
+            let mut partial_rng = rand::thread_rng();
+            let mut partial_writer = PartialInterruptingWriter {
+                w: &mut stream_encoded,
+                rng: &mut partial_rng,
+                full_input_fraction: 0.1,
+                no_interrupt_fraction: 0.1,
+            };
+
+            let mut stream_encoder = EncoderWriter::new(&mut partial_writer, config);
+            let mut bytes_consumed = 0;
+            while bytes_consumed < orig_len {
+                // use at most medium-length inputs to exercise retry logic more aggressively
+                let input_len: usize = cmp::min(rng.gen_range(0, 100), orig_len - bytes_consumed);
+
+                let res =
+                    stream_encoder.write(&orig_data[bytes_consumed..bytes_consumed + input_len]);
+
+                // retry on interrupt
+                match res {
+                    Ok(len) => bytes_consumed += len,
+                    Err(e) => match e.kind() {
+                        io::ErrorKind::Interrupted => continue,
+                        _ => {
+                            panic!("should not see other errors");
+                        }
+                    },
+                }
+            }
+
+            let _ = stream_encoder.finish().unwrap();
+
+            assert_eq!(orig_len, bytes_consumed);
+        }
+
+        assert_eq!(normal_encoded, str::from_utf8(&stream_encoded).unwrap());
+    }
+}
+
+/// Retry writes until all the data is written or an error that isn't Interrupted is returned.
+fn retry_interrupted_write_all<W: Write>(w: &mut W, buf: &[u8]) -> io::Result<()> {
+    let mut bytes_consumed = 0;
+
+    while bytes_consumed < buf.len() {
+        let res = w.write(&buf[bytes_consumed..]);
+
+        match res {
+            Ok(len) => bytes_consumed += len,
+            Err(e) => match e.kind() {
+                io::ErrorKind::Interrupted => continue,
+                _ => return Err(e),
+            },
+        }
+    }
+
+    Ok(())
+}
+
+fn do_encode_random_config_matches_normal_encode(max_input_len: usize) {
+    let mut rng = rand::thread_rng();
+    let mut orig_data = Vec::<u8>::new();
+    let mut stream_encoded = Vec::<u8>::new();
+    let mut normal_encoded = String::new();
+
+    for _ in 0..1_000 {
+        orig_data.clear();
+        stream_encoded.clear();
+        normal_encoded.clear();
+
+        let orig_len: usize = rng.gen_range(100, 20_000);
+        for _ in 0..orig_len {
+            orig_data.push(rng.gen());
+        }
+
+        // encode the normal way
+        let config = random_config(&mut rng);
+        encode_config_buf(&orig_data, config, &mut normal_encoded);
+
+        // encode via the stream encoder
+        {
+            let mut stream_encoder = EncoderWriter::new(&mut stream_encoded, config);
+            let mut bytes_consumed = 0;
+            while bytes_consumed < orig_len {
+                let input_len: usize =
+                    cmp::min(rng.gen_range(0, max_input_len), orig_len - bytes_consumed);
+
+                // write a little bit of the data
+                stream_encoder
+                    .write_all(&orig_data[bytes_consumed..bytes_consumed + input_len])
+                    .unwrap();
+
+                bytes_consumed += input_len;
+            }
+
+            let _ = stream_encoder.finish().unwrap();
+
+            assert_eq!(orig_len, bytes_consumed);
+        }
+
+        assert_eq!(normal_encoded, str::from_utf8(&stream_encoded).unwrap());
+    }
+}
+
+/// A `Write` implementation that returns Interrupted some fraction of the time, randomly.
+struct InterruptingWriter<'a, W: 'a + Write, R: 'a + Rng> {
+    w: &'a mut W,
+    rng: &'a mut R,
+    /// In [0, 1]. If a random number in [0, 1] is  `<= threshold`, `Write` methods will return
+    /// an `Interrupted` error
+    fraction: f64,
+}
+
+impl<'a, W: Write, R: Rng> Write for InterruptingWriter<'a, W, R> {
+    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+        if self.rng.gen_range(0.0, 1.0) <= self.fraction {
+            return Err(io::Error::new(io::ErrorKind::Interrupted, "interrupted"));
+        }
+
+        self.w.write(buf)
+    }
+
+    fn flush(&mut self) -> io::Result<()> {
+        if self.rng.gen_range(0.0, 1.0) <= self.fraction {
+            return Err(io::Error::new(io::ErrorKind::Interrupted, "interrupted"));
+        }
+
+        self.w.flush()
+    }
+}
+
+/// A `Write` implementation that sometimes will only write part of its input.
+struct PartialInterruptingWriter<'a, W: 'a + Write, R: 'a + Rng> {
+    w: &'a mut W,
+    rng: &'a mut R,
+    /// In [0, 1]. If a random number in [0, 1] is  `<= threshold`, `write()` will write all its
+    /// input. Otherwise, it will write a random substring
+    full_input_fraction: f64,
+    no_interrupt_fraction: f64,
+}
+
+impl<'a, W: Write, R: Rng> Write for PartialInterruptingWriter<'a, W, R> {
+    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+        if self.rng.gen_range(0.0, 1.0) > self.no_interrupt_fraction {
+            return Err(io::Error::new(io::ErrorKind::Interrupted, "interrupted"));
+        }
+
+        if self.rng.gen_range(0.0, 1.0) <= self.full_input_fraction || buf.len() == 0 {
+            // pass through the buf untouched
+            self.w.write(buf)
+        } else {
+            // only use a prefix of it
+            self.w
+                .write(&buf[0..(self.rng.gen_range(0, buf.len() - 1))])
+        }
+    }
+
+    fn flush(&mut self) -> io::Result<()> {
+        self.w.flush()
+    }
+}
diff --git a/src/write/mod.rs b/src/write/mod.rs
new file mode 100644
index 0000000..98cb48c
--- /dev/null
+++ b/src/write/mod.rs
@@ -0,0 +1,8 @@
+//! Implementations of `io::Write` to transparently handle base64.
+mod encoder;
+mod encoder_string_writer;
+pub use self::encoder::EncoderWriter;
+pub use self::encoder_string_writer::EncoderStringWriter;
+
+#[cfg(test)]
+mod encoder_tests;
diff --git a/tests/decode.rs b/tests/decode.rs
new file mode 100644
index 0000000..282bccd
--- /dev/null
+++ b/tests/decode.rs
@@ -0,0 +1,330 @@
+extern crate base64;
+
+use base64::*;
+
+mod helpers;
+
+use self::helpers::*;
+
+#[test]
+fn decode_rfc4648_0() {
+    compare_decode("", "");
+}
+
+#[test]
+fn decode_rfc4648_1() {
+    compare_decode("f", "Zg==");
+}
+
+#[test]
+fn decode_rfc4648_1_just_a_bit_of_padding() {
+    // allows less padding than required
+    compare_decode("f", "Zg=");
+}
+
+#[test]
+fn decode_rfc4648_1_no_padding() {
+    compare_decode("f", "Zg");
+}
+
+#[test]
+fn decode_rfc4648_2() {
+    compare_decode("fo", "Zm8=");
+}
+
+#[test]
+fn decode_rfc4648_2_no_padding() {
+    compare_decode("fo", "Zm8");
+}
+
+#[test]
+fn decode_rfc4648_3() {
+    compare_decode("foo", "Zm9v");
+}
+
+#[test]
+fn decode_rfc4648_4() {
+    compare_decode("foob", "Zm9vYg==");
+}
+
+#[test]
+fn decode_rfc4648_4_no_padding() {
+    compare_decode("foob", "Zm9vYg");
+}
+
+#[test]
+fn decode_rfc4648_5() {
+    compare_decode("fooba", "Zm9vYmE=");
+}
+
+#[test]
+fn decode_rfc4648_5_no_padding() {
+    compare_decode("fooba", "Zm9vYmE");
+}
+
+#[test]
+fn decode_rfc4648_6() {
+    compare_decode("foobar", "Zm9vYmFy");
+}
+
+#[test]
+fn decode_reject_null() {
+    assert_eq!(
+        DecodeError::InvalidByte(3, 0x0),
+        decode_config("YWx\0pY2U==", config_std_pad()).unwrap_err()
+    );
+}
+
+#[test]
+fn decode_single_pad_byte_after_2_chars_in_trailing_quad_ok() {
+    for num_quads in 0..25 {
+        let mut s: String = std::iter::repeat("ABCD").take(num_quads).collect();
+        s.push_str("Zg=");
+
+        let input_len = num_quads * 3 + 1;
+
+        // Since there are 3 bytes in the trailing quad, want to be sure this allows for the fact
+        // that it could be bad padding rather than assuming that it will decode to 2 bytes and
+        // therefore allow 1 extra round of fast decode logic (stage 1 / 2).
+
+        let mut decoded = Vec::new();
+        decoded.resize(input_len, 0);
+
+        assert_eq!(
+            input_len,
+            decode_config_slice(&s, STANDARD, &mut decoded).unwrap()
+        );
+    }
+}
+
+//this is a MAY in the rfc: https://tools.ietf.org/html/rfc4648#section-3.3
+#[test]
+fn decode_1_pad_byte_in_fast_loop_then_extra_padding_chunk_error() {
+    for num_quads in 0..25 {
+        let mut s: String = std::iter::repeat("ABCD").take(num_quads).collect();
+        s.push_str("YWxpY2U=====");
+
+        // since the first 8 bytes are handled in stage 1 or 2, the padding is detected as a
+        // generic invalid byte, not specifcally a padding issue.
+        // Could argue that the *next* padding byte (in the next quad) is technically the first
+        // erroneous one, but reporting that accurately is more complex and probably nobody cares
+        assert_eq!(
+            DecodeError::InvalidByte(num_quads * 4 + 7, b'='),
+            decode(&s).unwrap_err()
+        );
+    }
+}
+
+#[test]
+fn decode_2_pad_bytes_in_leftovers_then_extra_padding_chunk_error() {
+    for num_quads in 0..25 {
+        let mut s: String = std::iter::repeat("ABCD").take(num_quads).collect();
+        s.push_str("YWxpY2UABB====");
+
+        // 6 bytes (4 padding) after last 8-byte chunk, so it's decoded by stage 4.
+        // First padding byte is invalid.
+        assert_eq!(
+            DecodeError::InvalidByte(num_quads * 4 + 10, b'='),
+            decode(&s).unwrap_err()
+        );
+    }
+}
+
+#[test]
+fn decode_valid_bytes_after_padding_in_leftovers_error() {
+    for num_quads in 0..25 {
+        let mut s: String = std::iter::repeat("ABCD").take(num_quads).collect();
+        s.push_str("YWxpY2UABB=B");
+
+        // 4 bytes after last 8-byte chunk, so it's decoded by stage 4.
+        // First (and only) padding byte is invalid.
+        assert_eq!(
+            DecodeError::InvalidByte(num_quads * 4 + 10, b'='),
+            decode(&s).unwrap_err()
+        );
+    }
+}
+
+#[test]
+fn decode_absurd_pad_error() {
+    for num_quads in 0..25 {
+        let mut s: String = std::iter::repeat("ABCD").take(num_quads).collect();
+        s.push_str("==Y=Wx===pY=2U=====");
+
+        // Plenty of remaining bytes, so handled by stage 1 or 2.
+        // first padding byte
+        assert_eq!(
+            DecodeError::InvalidByte(num_quads * 4, b'='),
+            decode(&s).unwrap_err()
+        );
+    }
+}
+
+#[test]
+fn decode_extra_padding_after_1_pad_bytes_in_trailing_quad_returns_error() {
+    for num_quads in 0..25 {
+        let mut s: String = std::iter::repeat("ABCD").take(num_quads).collect();
+        s.push_str("EEE===");
+
+        // handled by stage 1, 2, or 4 depending on length
+        // first padding byte -- which would be legal if it was the only padding
+        assert_eq!(
+            DecodeError::InvalidByte(num_quads * 4 + 3, b'='),
+            decode(&s).unwrap_err()
+        );
+    }
+}
+
+#[test]
+fn decode_extra_padding_after_2_pad_bytes_in_trailing_quad_2_returns_error() {
+    for num_quads in 0..25 {
+        let mut s: String = std::iter::repeat("ABCD").take(num_quads).collect();
+        s.push_str("EE====");
+
+        // handled by stage 1, 2, or 4 depending on length
+        // first padding byte -- which would be legal if it was by itself
+        assert_eq!(
+            DecodeError::InvalidByte(num_quads * 4 + 2, b'='),
+            decode(&s).unwrap_err()
+        );
+    }
+}
+
+#[test]
+fn decode_start_quad_with_padding_returns_error() {
+    for num_quads in 0..25 {
+        // add enough padding to ensure that we'll hit all 4 stages at the different lengths
+        for pad_bytes in 1..32 {
+            let mut s: String = std::iter::repeat("ABCD").take(num_quads).collect();
+            let padding: String = std::iter::repeat("=").take(pad_bytes).collect();
+            s.push_str(&padding);
+
+            if pad_bytes % 4 == 1 {
+                // detected in early length check
+                assert_eq!(DecodeError::InvalidLength, decode(&s).unwrap_err());
+            } else {
+                // padding lengths 2 - 8 are handled by stage 4
+                // padding length >= 8 will hit at least one chunk at stages 1, 2, 3 at different
+                // prefix lengths
+                assert_eq!(
+                    DecodeError::InvalidByte(num_quads * 4, b'='),
+                    decode(&s).unwrap_err()
+                );
+            }
+        }
+    }
+}
+
+#[test]
+fn decode_padding_followed_by_non_padding_returns_error() {
+    for num_quads in 0..25 {
+        for pad_bytes in 0..31 {
+            let mut s: String = std::iter::repeat("ABCD").take(num_quads).collect();
+            let padding: String = std::iter::repeat("=").take(pad_bytes).collect();
+            s.push_str(&padding);
+            s.push_str("E");
+
+            if pad_bytes % 4 == 0 {
+                assert_eq!(DecodeError::InvalidLength, decode(&s).unwrap_err());
+            } else {
+                // pad len 1 - 8 will be handled by stage 4
+                // pad len 9 (suffix len 10) will have 8 bytes of padding handled by stage 3
+                // first padding byte
+                assert_eq!(
+                    DecodeError::InvalidByte(num_quads * 4, b'='),
+                    decode(&s).unwrap_err()
+                );
+            }
+        }
+    }
+}
+
+#[test]
+fn decode_one_char_in_quad_with_padding_error() {
+    for num_quads in 0..25 {
+        let mut s: String = std::iter::repeat("ABCD").take(num_quads).collect();
+        s.push_str("E=");
+
+        assert_eq!(
+            DecodeError::InvalidByte(num_quads * 4 + 1, b'='),
+            decode(&s).unwrap_err()
+        );
+
+        // more padding doesn't change the error
+        s.push_str("=");
+        assert_eq!(
+            DecodeError::InvalidByte(num_quads * 4 + 1, b'='),
+            decode(&s).unwrap_err()
+        );
+
+        s.push_str("=");
+        assert_eq!(
+            DecodeError::InvalidByte(num_quads * 4 + 1, b'='),
+            decode(&s).unwrap_err()
+        );
+    }
+}
+
+#[test]
+fn decode_one_char_in_quad_without_padding_error() {
+    for num_quads in 0..25 {
+        let mut s: String = std::iter::repeat("ABCD").take(num_quads).collect();
+        s.push('E');
+
+        assert_eq!(DecodeError::InvalidLength, decode(&s).unwrap_err());
+    }
+}
+
+#[test]
+fn decode_reject_invalid_bytes_with_correct_error() {
+    for length in 1..100 {
+        for index in 0_usize..length {
+            for invalid_byte in " \t\n\r\x0C\x0B\x00%*.".bytes() {
+                let prefix: String = std::iter::repeat("A").take(index).collect();
+                let suffix: String = std::iter::repeat("B").take(length - index - 1).collect();
+
+                let input = prefix + &String::from_utf8(vec![invalid_byte]).unwrap() + &suffix;
+                assert_eq!(
+                    length,
+                    input.len(),
+                    "length {} error position {}",
+                    length,
+                    index
+                );
+
+                if length % 4 == 1 && !suffix.is_empty() {
+                    assert_eq!(DecodeError::InvalidLength, decode(&input).unwrap_err());
+                } else {
+                    assert_eq!(
+                        DecodeError::InvalidByte(index, invalid_byte),
+                        decode(&input).unwrap_err()
+                    );
+                }
+            }
+        }
+    }
+}
+
+#[test]
+fn decode_imap() {
+    assert_eq!(
+        decode_config(b"+,,+", crate::IMAP_MUTF7),
+        decode_config(b"+//+", crate::STANDARD_NO_PAD)
+    );
+}
+
+#[test]
+fn decode_invalid_trailing_bytes() {
+    // The case of trailing newlines is common enough to warrant a test for a good error
+    // message.
+    assert_eq!(
+        Err(DecodeError::InvalidByte(8, b'\n')),
+        decode(b"Zm9vCg==\n")
+    );
+    // extra padding, however, is still InvalidLength
+    assert_eq!(Err(DecodeError::InvalidLength), decode(b"Zm9vCg==="));
+}
+
+fn config_std_pad() -> Config {
+    Config::new(CharacterSet::Standard, true)
+}
diff --git a/tests/encode.rs b/tests/encode.rs
new file mode 100644
index 0000000..0004be0
--- /dev/null
+++ b/tests/encode.rs
@@ -0,0 +1,105 @@
+extern crate base64;
+
+use base64::*;
+
+fn compare_encode(expected: &str, target: &[u8]) {
+    assert_eq!(expected, encode(target));
+}
+
+#[test]
+fn encode_rfc4648_0() {
+    compare_encode("", b"");
+}
+
+#[test]
+fn encode_rfc4648_1() {
+    compare_encode("Zg==", b"f");
+}
+
+#[test]
+fn encode_rfc4648_2() {
+    compare_encode("Zm8=", b"fo");
+}
+
+#[test]
+fn encode_rfc4648_3() {
+    compare_encode("Zm9v", b"foo");
+}
+
+#[test]
+fn encode_rfc4648_4() {
+    compare_encode("Zm9vYg==", b"foob");
+}
+
+#[test]
+fn encode_rfc4648_5() {
+    compare_encode("Zm9vYmE=", b"fooba");
+}
+
+#[test]
+fn encode_rfc4648_6() {
+    compare_encode("Zm9vYmFy", b"foobar");
+}
+
+#[test]
+fn encode_all_ascii() {
+    let mut ascii = Vec::<u8>::with_capacity(128);
+
+    for i in 0..128 {
+        ascii.push(i);
+    }
+
+    compare_encode(
+        "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7P\
+         D0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn8\
+         =",
+        &ascii,
+    );
+}
+
+#[test]
+fn encode_all_bytes() {
+    let mut bytes = Vec::<u8>::with_capacity(256);
+
+    for i in 0..255 {
+        bytes.push(i);
+    }
+    bytes.push(255); //bug with "overflowing" ranges?
+
+    compare_encode(
+        "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7P\
+         D0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn\
+         +AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6\
+         /wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w==",
+        &bytes,
+    );
+}
+
+#[test]
+fn encode_all_bytes_url() {
+    let mut bytes = Vec::<u8>::with_capacity(256);
+
+    for i in 0..255 {
+        bytes.push(i);
+    }
+    bytes.push(255); //bug with "overflowing" ranges?
+
+    assert_eq!(
+        "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0\
+         -P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn\
+         -AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq\
+         -wsbKztLW2t7i5uru8vb6_wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t_g4eLj5OXm5-jp6uvs7e7v8PHy\
+         8_T19vf4-fr7_P3-_w==",
+        encode_config(&bytes, URL_SAFE)
+    );
+}
+
+#[test]
+fn encode_url_safe_without_padding() {
+    let encoded = encode_config(b"alice", URL_SAFE_NO_PAD);
+    assert_eq!(&encoded, "YWxpY2U");
+    assert_eq!(
+        String::from_utf8(decode(&encoded).unwrap()).unwrap(),
+        "alice"
+    );
+}
diff --git a/tests/helpers.rs b/tests/helpers.rs
new file mode 100644
index 0000000..5144988
--- /dev/null
+++ b/tests/helpers.rs
@@ -0,0 +1,14 @@
+extern crate base64;
+
+use base64::*;
+
+pub fn compare_decode(expected: &str, target: &str) {
+    assert_eq!(
+        expected,
+        String::from_utf8(decode(target).unwrap()).unwrap()
+    );
+    assert_eq!(
+        expected,
+        String::from_utf8(decode(target.as_bytes()).unwrap()).unwrap()
+    );
+}
diff --git a/tests/tests.rs b/tests/tests.rs
new file mode 100644
index 0000000..11fed96
--- /dev/null
+++ b/tests/tests.rs
@@ -0,0 +1,194 @@
+extern crate base64;
+extern crate rand;
+
+use rand::{FromEntropy, Rng};
+
+use base64::*;
+
+mod helpers;
+use self::helpers::*;
+
+// generate random contents of the specified length and test encode/decode roundtrip
+fn roundtrip_random(
+    byte_buf: &mut Vec<u8>,
+    str_buf: &mut String,
+    config: Config,
+    byte_len: usize,
+    approx_values_per_byte: u8,
+    max_rounds: u64,
+) {
+    // let the short ones be short but don't let it get too crazy large
+    let num_rounds = calculate_number_of_rounds(byte_len, approx_values_per_byte, max_rounds);
+    let mut r = rand::rngs::SmallRng::from_entropy();
+    let mut decode_buf = Vec::new();
+
+    for _ in 0..num_rounds {
+        byte_buf.clear();
+        str_buf.clear();
+        decode_buf.clear();
+        while byte_buf.len() < byte_len {
+            byte_buf.push(r.gen::<u8>());
+        }
+
+        encode_config_buf(&byte_buf, config, str_buf);
+        decode_config_buf(&str_buf, config, &mut decode_buf).unwrap();
+
+        assert_eq!(byte_buf, &decode_buf);
+    }
+}
+
+fn calculate_number_of_rounds(byte_len: usize, approx_values_per_byte: u8, max: u64) -> u64 {
+    // don't overflow
+    let mut prod = approx_values_per_byte as u64;
+
+    for _ in 0..byte_len {
+        if prod > max {
+            return max;
+        }
+
+        prod = prod.saturating_mul(prod);
+    }
+
+    prod
+}
+
+fn no_pad_config() -> Config {
+    Config::new(CharacterSet::Standard, false)
+}
+
+#[test]
+fn roundtrip_random_short_standard() {
+    let mut byte_buf: Vec<u8> = Vec::new();
+    let mut str_buf = String::new();
+
+    for input_len in 0..40 {
+        roundtrip_random(&mut byte_buf, &mut str_buf, STANDARD, input_len, 4, 10000);
+    }
+}
+
+#[test]
+fn roundtrip_random_with_fast_loop_standard() {
+    let mut byte_buf: Vec<u8> = Vec::new();
+    let mut str_buf = String::new();
+
+    for input_len in 40..100 {
+        roundtrip_random(&mut byte_buf, &mut str_buf, STANDARD, input_len, 4, 1000);
+    }
+}
+
+#[test]
+fn roundtrip_random_short_no_padding() {
+    let mut byte_buf: Vec<u8> = Vec::new();
+    let mut str_buf = String::new();
+
+    for input_len in 0..40 {
+        roundtrip_random(
+            &mut byte_buf,
+            &mut str_buf,
+            no_pad_config(),
+            input_len,
+            4,
+            10000,
+        );
+    }
+}
+
+#[test]
+fn roundtrip_random_no_padding() {
+    let mut byte_buf: Vec<u8> = Vec::new();
+    let mut str_buf = String::new();
+
+    for input_len in 40..100 {
+        roundtrip_random(
+            &mut byte_buf,
+            &mut str_buf,
+            no_pad_config(),
+            input_len,
+            4,
+            1000,
+        );
+    }
+}
+
+#[test]
+fn roundtrip_decode_trailing_10_bytes() {
+    // This is a special case because we decode 8 byte blocks of input at a time as much as we can,
+    // ideally unrolled to 32 bytes at a time, in stages 1 and 2. Since we also write a u64's worth
+    // of bytes (8) to the output, we always write 2 garbage bytes that then will be overwritten by
+    // the NEXT block. However, if the next block only contains 2 bytes, it will decode to 1 byte,
+    // and therefore be too short to cover up the trailing 2 garbage bytes. Thus, we have stage 3
+    // to handle that case.
+
+    for num_quads in 0..25 {
+        let mut s: String = std::iter::repeat("ABCD").take(num_quads).collect();
+        s.push_str("EFGHIJKLZg");
+
+        let decoded = decode(&s).unwrap();
+        assert_eq!(num_quads * 3 + 7, decoded.len());
+
+        assert_eq!(s, encode_config(&decoded, STANDARD_NO_PAD));
+    }
+}
+
+#[test]
+fn display_wrapper_matches_normal_encode() {
+    let mut bytes = Vec::<u8>::with_capacity(256);
+
+    for i in 0..255 {
+        bytes.push(i);
+    }
+    bytes.push(255);
+
+    assert_eq!(
+        encode(&bytes),
+        format!(
+            "{}",
+            base64::display::Base64Display::with_config(&bytes, STANDARD)
+        )
+    );
+}
+
+#[test]
+fn because_we_can() {
+    compare_decode("alice", "YWxpY2U=");
+    compare_decode("alice", &encode(b"alice"));
+    compare_decode("alice", &encode(&decode(&encode(b"alice")).unwrap()));
+}
+
+#[test]
+fn encode_config_slice_can_use_inline_buffer() {
+    let mut buf: [u8; 22] = [0; 22];
+    let mut larger_buf: [u8; 24] = [0; 24];
+    let mut input: [u8; 16] = [0; 16];
+
+    let mut rng = rand::rngs::SmallRng::from_entropy();
+    for elt in &mut input {
+        *elt = rng.gen();
+    }
+
+    assert_eq!(22, encode_config_slice(&input, STANDARD_NO_PAD, &mut buf));
+    let decoded = decode_config(&buf, STANDARD_NO_PAD).unwrap();
+
+    assert_eq!(decoded, input);
+
+    // let's try it again with padding
+
+    assert_eq!(24, encode_config_slice(&input, STANDARD, &mut larger_buf));
+    let decoded = decode_config(&buf, STANDARD).unwrap();
+
+    assert_eq!(decoded, input);
+}
+
+#[test]
+#[should_panic(expected = "index 24 out of range for slice of length 22")]
+fn encode_config_slice_panics_when_buffer_too_small() {
+    let mut buf: [u8; 22] = [0; 22];
+    let mut input: [u8; 16] = [0; 16];
+
+    let mut rng = rand::rngs::SmallRng::from_entropy();
+    for elt in &mut input {
+        *elt = rng.gen();
+    }
+
+    encode_config_slice(&input, STANDARD, &mut buf);
+}