pw_base64: Module for Base64 encoding and decoding

Provides C and C++ functions for encoding, decoding, and checking the
validity of Base64 data.

Change-Id: I784460d802c68b01ecee5a1778626f289685a871
diff --git a/BUILD.gn b/BUILD.gn
index 1b37dd6..c30696b 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -49,6 +49,7 @@
 group("pw_modules") {
   deps = [
     "$dir_pigweed/docs",
+    "$dir_pw_base64",
     "$dir_pw_preprocessor",
     "$dir_pw_protobuf",
     "$dir_pw_span",
@@ -62,6 +63,7 @@
 # Targets for all module unit test groups.
 pw_test_group("pw_module_tests") {
   group_deps = [
+    "$dir_pw_base64:tests",
     "$dir_pw_preprocessor:tests",
     "$dir_pw_protobuf:tests",
     "$dir_pw_span:tests",
diff --git a/CMakeLists.txt b/CMakeLists.txt
index d9b215a..451bf7c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -18,6 +18,7 @@
 
 include(pw_build/pigweed.cmake)
 
+add_subdirectory(pw_base64)
 add_subdirectory(pw_cpu_exception)
 add_subdirectory(pw_cpu_exception_armv7m)
 add_subdirectory(pw_dumb_io)
diff --git a/modules.gni b/modules.gni
index 348e5e0..59faf73 100644
--- a/modules.gni
+++ b/modules.gni
@@ -16,6 +16,7 @@
 # allows modules to be moved or swapped out without breaking existing builds.
 # All module variables are prefixed with dir_.
 
+dir_pw_base64 = "$dir_pigweed/pw_base64"
 dir_pw_bloat = "$dir_pigweed/pw_bloat"
 dir_pw_build = "$dir_pigweed/pw_build"
 dir_pw_cpu_exception = "$dir_pigweed/pw_cpu_exception"
diff --git a/pw_base64/BUILD b/pw_base64/BUILD
new file mode 100644
index 0000000..062556c
--- /dev/null
+++ b/pw_base64/BUILD
@@ -0,0 +1,48 @@
+# Copyright 2020 The Pigweed Authors
+#
+# 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
+#
+#     https://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.
+
+load(
+    "//pw_build:pigweed.bzl",
+    "pw_cc_library",
+    "pw_cc_test",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])  # Apache License 2.0
+
+pw_cc_library(
+    name = "pw_base64",
+    srcs = [
+        "base64.cc",
+    ],
+    hdrs = [
+        "public/pw_base64/base64.h",
+    ],
+    includes = ["public"],
+    deps = [
+        "//pw_span",
+    ],
+)
+
+pw_cc_test(
+    name = "base64_test",
+    srcs = [
+        "base64_test.c",
+        "base64_test.cc",
+    ],
+    deps = [
+        ":pw_base64",
+    ],
+)
diff --git a/pw_base64/BUILD.gn b/pw_base64/BUILD.gn
new file mode 100644
index 0000000..689e94c
--- /dev/null
+++ b/pw_base64/BUILD.gn
@@ -0,0 +1,51 @@
+# Copyright 2020 The Pigweed Authors
+#
+# 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
+#
+#     https://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.
+
+import("$dir_pw_unit_test/test.gni")
+
+config("default_config") {
+  include_dirs = [ "public" ]
+}
+
+source_set("pw_base64") {
+  public_configs = [
+    "$dir_pw_build:pw_default_cpp",
+    ":default_config",
+  ]
+  public = [
+    "public/pw_base64/base64.h",
+  ]
+  sources = [
+    "base64.cc",
+  ]
+  sources += public
+  public_deps = [
+    "$dir_pw_span",
+  ]
+}
+
+pw_test_group("tests") {
+  tests = [ ":base64_test" ]
+  group_deps = [ "$dir_pw_span:tests" ]
+}
+
+pw_test("base64_test") {
+  deps = [
+    ":pw_base64",
+  ]
+  sources = [
+    "base64_test.c",
+    "base64_test.cc",
+  ]
+}
diff --git a/pw_base64/CMakeLists.txt b/pw_base64/CMakeLists.txt
new file mode 100644
index 0000000..95e63a9
--- /dev/null
+++ b/pw_base64/CMakeLists.txt
@@ -0,0 +1,18 @@
+# Copyright 2020 The Pigweed Authors
+#
+# 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
+#
+#     https://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.
+
+pw_auto_add_simple_module(pw_base64
+  PUBLIC_DEPS
+    pw_span
+)
diff --git a/pw_base64/base64.cc b/pw_base64/base64.cc
new file mode 100644
index 0000000..c708bb5
--- /dev/null
+++ b/pw_base64/base64.cc
@@ -0,0 +1,174 @@
+// Copyright 2020 The Pigweed Authors
+//
+// 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
+//
+//     https://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.
+
+#include "pw_base64/base64.h"
+
+#include <cstdint>
+
+namespace pw::base64 {
+namespace {
+
+// Encoding functions
+constexpr size_t kEncodedGroupSize = 4;
+constexpr char kChar62 = '+';  // URL safe encoding uses - instead
+constexpr char kChar63 = '/';  // URL safe encoding uses _ instead
+constexpr char kPadding = '=';
+
+// Table that encodes a 6-bit pattern as a Base64 character
+constexpr char encode_bits[64] = {
+    'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K',     'L',    'M',
+    'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',     'Y',    'Z',
+    'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',     'l',    'm',
+    'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x',     'y',    'z',
+    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', kChar62, kChar63};
+
+constexpr char BitGroup0Char(uint8_t byte0) {
+  return encode_bits[(byte0 & 0b11111100) >> 2];
+}
+constexpr char BitGroup1Char(uint8_t byte0, uint8_t byte1 = 0) {
+  return encode_bits[((byte0 & 0b00000011) << 4) | ((byte1 & 0b11110000) >> 4)];
+}
+constexpr char BitGroup2Char(uint8_t byte1, uint8_t byte2 = 0) {
+  return encode_bits[((byte1 & 0b00001111) << 2) | ((byte2 & 0b11000000) >> 6)];
+}
+constexpr char BitGroup3Char(uint8_t byte2) {
+  return encode_bits[byte2 & 0b00111111];
+}
+
+// Decoding functions
+constexpr char kMinValidChar = '+';
+constexpr char kMaxValidChar = 'z';
+constexpr uint8_t kX = 0xff;  // Value used for invalid characters
+
+// Table that decodes a Base64 character to its 6-bit value. Supports the
+// standard (+/) and URL-safe (-_) alphabets. Starts from the lowest-value valid
+// character, which is +.
+constexpr uint8_t decode_char[] = {
+    62, kX, 62, kX, 63, 52, 53, 54, 55, 56,  //  0 - 09
+    57, 58, 59, 60, 61, kX, kX, kX, 0,  kX,  // 10 - 19
+    kX, kX, 0,  1,  2,  3,  4,  5,  6,  7,   // 20 - 29
+    8,  9,  10, 11, 12, 13, 14, 15, 16, 17,  // 30 - 39
+    18, 19, 20, 21, 22, 23, 24, 25, kX, kX,  // 40 - 49
+    kX, kX, 63, kX, 26, 27, 28, 29, 30, 31,  // 50 - 59
+    32, 33, 34, 35, 36, 37, 38, 39, 40, 41,  // 60 - 69
+    42, 43, 44, 45, 46, 47, 48, 49, 50, 51,  // 70 - 79
+};
+
+constexpr uint8_t CharToBits(char ch) {
+  return decode_char[ch - kMinValidChar];
+}
+
+constexpr uint8_t Byte0(uint8_t bits0, uint8_t bits1) {
+  return (bits0 << 2) | ((bits1 & 0b110000) >> 4);
+}
+constexpr uint8_t Byte1(uint8_t bits1, uint8_t bits2) {
+  return ((bits1 & 0b001111) << 4) | ((bits2 & 0b111100) >> 2);
+}
+constexpr uint8_t Byte2(uint8_t bits2, uint8_t bits3) {
+  return ((bits2 & 0b000011) << 6) | bits3;
+}
+
+}  // namespace
+
+extern "C" void pw_Base64Encode(const void* binary_data,
+                                const size_t binary_size_bytes,
+                                char* output) {
+  const uint8_t* bytes = static_cast<const uint8_t*>(binary_data);
+
+  // Encode groups of 3 source bytes into 4 output characters.
+  size_t remaining = binary_size_bytes;
+  for (; remaining >= 3u; remaining -= 3u, bytes += 3) {
+    *output++ = BitGroup0Char(bytes[0]);
+    *output++ = BitGroup1Char(bytes[0], bytes[1]);
+    *output++ = BitGroup2Char(bytes[1], bytes[2]);
+    *output++ = BitGroup3Char(bytes[2]);
+  }
+
+  // If the source data length isn't a multiple of 3, pad the end with either 1
+  // or 2 '=' characters, to stay Python-compatible.
+  if (remaining > 0u) {
+    *output++ = BitGroup0Char(bytes[0]);
+    if (remaining == 1u) {
+      *output++ = BitGroup1Char(bytes[0]);
+      *output++ = kPadding;
+    } else {
+      *output++ = BitGroup1Char(bytes[0], bytes[1]);
+      *output++ = BitGroup2Char(bytes[1]);
+    }
+    *output++ = kPadding;
+  }
+}
+
+extern "C" size_t pw_Base64Decode(const char* base64,
+                                  size_t base64_size_bytes,
+                                  void* output) {
+  // If too small, can't be valid input, due to likely missing padding
+  if (base64_size_bytes < 4) {
+    return 0;
+  }
+
+  uint8_t* binary = static_cast<uint8_t*>(output);
+  for (size_t ch = 0; ch < base64_size_bytes; ch += kEncodedGroupSize) {
+    const uint8_t char0 = CharToBits(base64[ch + 0]);
+    const uint8_t char1 = CharToBits(base64[ch + 1]);
+    const uint8_t char2 = CharToBits(base64[ch + 2]);
+    const uint8_t char3 = CharToBits(base64[ch + 3]);
+
+    *binary++ = Byte0(char0, char1);
+    *binary++ = Byte1(char1, char2);
+    *binary++ = Byte2(char2, char3);
+  }
+
+  size_t pad = 0;
+  if (base64[base64_size_bytes - 2] == kPadding) {
+    pad = 2;
+  } else if (base64[base64_size_bytes - 1] == kPadding) {
+    pad = 1;
+  }
+
+  return binary - static_cast<uint8_t*>(output) - pad;
+}
+
+extern "C" bool pw_Base64IsValid(const char* base64_data, size_t base64_size) {
+  if (base64_size % kEncodedGroupSize != 0) {
+    return false;
+  }
+
+  for (size_t i = 0; i < base64_size; ++i) {
+    if (base64_data[i] < kMinValidChar || base64_data[i] > kMaxValidChar ||
+        CharToBits(base64_data[i]) == kX /* invalid char */) {
+      return false;
+    }
+  }
+  return true;
+}
+
+size_t Encode(span<const std::byte> binary, span<char> output_buffer) {
+  const size_t required_size = EncodedSize(binary.size_bytes());
+  if (output_buffer.size_bytes() < required_size) {
+    return 0;
+  }
+  pw_Base64Encode(binary.data(), binary.size_bytes(), output_buffer.data());
+  return required_size;
+}
+
+size_t Decode(std::string_view base64, span<std::byte> output_buffer) {
+  if (output_buffer.size_bytes() < MaxDecodedSize(base64.size()) ||
+      !IsValid(base64)) {
+    return 0;
+  }
+  return Decode(base64, output_buffer.data());
+}
+
+}  // namespace pw::base64
diff --git a/pw_base64/base64_test.c b/pw_base64/base64_test.c
new file mode 100644
index 0000000..be772b8
--- /dev/null
+++ b/pw_base64/base64_test.c
@@ -0,0 +1,38 @@
+// Copyright 2020 The Pigweed Authors
+//
+// 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
+//
+//     https://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.
+
+// These tests call the pw_base64 module API from C. The return values are
+// checked in the main C++ tests.
+//
+// The encoded / decoded size macros are tested in the main C++ tests.
+
+#include "pw_base64/base64.h"
+
+#include <stddef.h>
+
+void pw_Base64CallEncode(const void* binary_data,
+                         const size_t binary_size_bytes,
+                         char* output) {
+  return pw_Base64Encode(binary_data, binary_size_bytes, output);
+}
+
+size_t pw_Base64CallDecode(const char* base64,
+                           size_t base64_size_bytes,
+                           void* output) {
+  return pw_Base64Decode(base64, base64_size_bytes, output);
+}
+
+bool pw_Base64CallIsValid(const char* base64_data, size_t base64_size) {
+  return pw_Base64IsValid(base64_data, base64_size);
+}
diff --git a/pw_base64/base64_test.cc b/pw_base64/base64_test.cc
new file mode 100644
index 0000000..0dfcfd7
--- /dev/null
+++ b/pw_base64/base64_test.cc
@@ -0,0 +1,501 @@
+// Copyright 2020 The Pigweed Authors
+//
+// 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
+//
+//     https://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.
+
+#include "pw_base64/base64.h"
+
+#include <cstring>
+
+#include "gtest/gtest.h"
+
+namespace pw::base64 {
+namespace {
+
+struct EncodedData {
+  const size_t binary_size;
+  const char* const binary_data;
+  const char* const encoded_data;
+};
+
+/* The following test data was generated by this Python 3 script.
+
+#!/usr/bin/env python3
+
+import base64
+import random
+
+
+def b64_encode(raw_data):
+    encoded = base64.b64encode(raw_data).decode()
+    hex_string = ''.join(r'\x{:02x}'.format(b) for b in raw_data)
+
+    print('    {{{size}, "{raw}", "{encoded}"}},'.format(
+        size=len(raw_data), raw=hex_string, encoded=encoded))
+
+
+print('constexpr EncodedData kSingleCharTestData[] = {')
+
+for i in range(256):
+    b64_encode(bytes([i]))
+
+print('};')
+print()
+
+print('constexpr EncodedData kRandomTestData[] = {')
+
+for length in range(2, 12):
+    for _ in range(10):
+        b64_encode(bytes(random.randrange(256) for _ in range(length)))
+
+print('};')
+print()
+
+*/
+
+constexpr EncodedData kSingleCharTestData[] = {
+    {1, "\x00", "AA=="}, {1, "\x01", "AQ=="}, {1, "\x02", "Ag=="},
+    {1, "\x03", "Aw=="}, {1, "\x04", "BA=="}, {1, "\x05", "BQ=="},
+    {1, "\x06", "Bg=="}, {1, "\x07", "Bw=="}, {1, "\x08", "CA=="},
+    {1, "\x09", "CQ=="}, {1, "\x0a", "Cg=="}, {1, "\x0b", "Cw=="},
+    {1, "\x0c", "DA=="}, {1, "\x0d", "DQ=="}, {1, "\x0e", "Dg=="},
+    {1, "\x0f", "Dw=="}, {1, "\x10", "EA=="}, {1, "\x11", "EQ=="},
+    {1, "\x12", "Eg=="}, {1, "\x13", "Ew=="}, {1, "\x14", "FA=="},
+    {1, "\x15", "FQ=="}, {1, "\x16", "Fg=="}, {1, "\x17", "Fw=="},
+    {1, "\x18", "GA=="}, {1, "\x19", "GQ=="}, {1, "\x1a", "Gg=="},
+    {1, "\x1b", "Gw=="}, {1, "\x1c", "HA=="}, {1, "\x1d", "HQ=="},
+    {1, "\x1e", "Hg=="}, {1, "\x1f", "Hw=="}, {1, "\x20", "IA=="},
+    {1, "\x21", "IQ=="}, {1, "\x22", "Ig=="}, {1, "\x23", "Iw=="},
+    {1, "\x24", "JA=="}, {1, "\x25", "JQ=="}, {1, "\x26", "Jg=="},
+    {1, "\x27", "Jw=="}, {1, "\x28", "KA=="}, {1, "\x29", "KQ=="},
+    {1, "\x2a", "Kg=="}, {1, "\x2b", "Kw=="}, {1, "\x2c", "LA=="},
+    {1, "\x2d", "LQ=="}, {1, "\x2e", "Lg=="}, {1, "\x2f", "Lw=="},
+    {1, "\x30", "MA=="}, {1, "\x31", "MQ=="}, {1, "\x32", "Mg=="},
+    {1, "\x33", "Mw=="}, {1, "\x34", "NA=="}, {1, "\x35", "NQ=="},
+    {1, "\x36", "Ng=="}, {1, "\x37", "Nw=="}, {1, "\x38", "OA=="},
+    {1, "\x39", "OQ=="}, {1, "\x3a", "Og=="}, {1, "\x3b", "Ow=="},
+    {1, "\x3c", "PA=="}, {1, "\x3d", "PQ=="}, {1, "\x3e", "Pg=="},
+    {1, "\x3f", "Pw=="}, {1, "\x40", "QA=="}, {1, "\x41", "QQ=="},
+    {1, "\x42", "Qg=="}, {1, "\x43", "Qw=="}, {1, "\x44", "RA=="},
+    {1, "\x45", "RQ=="}, {1, "\x46", "Rg=="}, {1, "\x47", "Rw=="},
+    {1, "\x48", "SA=="}, {1, "\x49", "SQ=="}, {1, "\x4a", "Sg=="},
+    {1, "\x4b", "Sw=="}, {1, "\x4c", "TA=="}, {1, "\x4d", "TQ=="},
+    {1, "\x4e", "Tg=="}, {1, "\x4f", "Tw=="}, {1, "\x50", "UA=="},
+    {1, "\x51", "UQ=="}, {1, "\x52", "Ug=="}, {1, "\x53", "Uw=="},
+    {1, "\x54", "VA=="}, {1, "\x55", "VQ=="}, {1, "\x56", "Vg=="},
+    {1, "\x57", "Vw=="}, {1, "\x58", "WA=="}, {1, "\x59", "WQ=="},
+    {1, "\x5a", "Wg=="}, {1, "\x5b", "Ww=="}, {1, "\x5c", "XA=="},
+    {1, "\x5d", "XQ=="}, {1, "\x5e", "Xg=="}, {1, "\x5f", "Xw=="},
+    {1, "\x60", "YA=="}, {1, "\x61", "YQ=="}, {1, "\x62", "Yg=="},
+    {1, "\x63", "Yw=="}, {1, "\x64", "ZA=="}, {1, "\x65", "ZQ=="},
+    {1, "\x66", "Zg=="}, {1, "\x67", "Zw=="}, {1, "\x68", "aA=="},
+    {1, "\x69", "aQ=="}, {1, "\x6a", "ag=="}, {1, "\x6b", "aw=="},
+    {1, "\x6c", "bA=="}, {1, "\x6d", "bQ=="}, {1, "\x6e", "bg=="},
+    {1, "\x6f", "bw=="}, {1, "\x70", "cA=="}, {1, "\x71", "cQ=="},
+    {1, "\x72", "cg=="}, {1, "\x73", "cw=="}, {1, "\x74", "dA=="},
+    {1, "\x75", "dQ=="}, {1, "\x76", "dg=="}, {1, "\x77", "dw=="},
+    {1, "\x78", "eA=="}, {1, "\x79", "eQ=="}, {1, "\x7a", "eg=="},
+    {1, "\x7b", "ew=="}, {1, "\x7c", "fA=="}, {1, "\x7d", "fQ=="},
+    {1, "\x7e", "fg=="}, {1, "\x7f", "fw=="}, {1, "\x80", "gA=="},
+    {1, "\x81", "gQ=="}, {1, "\x82", "gg=="}, {1, "\x83", "gw=="},
+    {1, "\x84", "hA=="}, {1, "\x85", "hQ=="}, {1, "\x86", "hg=="},
+    {1, "\x87", "hw=="}, {1, "\x88", "iA=="}, {1, "\x89", "iQ=="},
+    {1, "\x8a", "ig=="}, {1, "\x8b", "iw=="}, {1, "\x8c", "jA=="},
+    {1, "\x8d", "jQ=="}, {1, "\x8e", "jg=="}, {1, "\x8f", "jw=="},
+    {1, "\x90", "kA=="}, {1, "\x91", "kQ=="}, {1, "\x92", "kg=="},
+    {1, "\x93", "kw=="}, {1, "\x94", "lA=="}, {1, "\x95", "lQ=="},
+    {1, "\x96", "lg=="}, {1, "\x97", "lw=="}, {1, "\x98", "mA=="},
+    {1, "\x99", "mQ=="}, {1, "\x9a", "mg=="}, {1, "\x9b", "mw=="},
+    {1, "\x9c", "nA=="}, {1, "\x9d", "nQ=="}, {1, "\x9e", "ng=="},
+    {1, "\x9f", "nw=="}, {1, "\xa0", "oA=="}, {1, "\xa1", "oQ=="},
+    {1, "\xa2", "og=="}, {1, "\xa3", "ow=="}, {1, "\xa4", "pA=="},
+    {1, "\xa5", "pQ=="}, {1, "\xa6", "pg=="}, {1, "\xa7", "pw=="},
+    {1, "\xa8", "qA=="}, {1, "\xa9", "qQ=="}, {1, "\xaa", "qg=="},
+    {1, "\xab", "qw=="}, {1, "\xac", "rA=="}, {1, "\xad", "rQ=="},
+    {1, "\xae", "rg=="}, {1, "\xaf", "rw=="}, {1, "\xb0", "sA=="},
+    {1, "\xb1", "sQ=="}, {1, "\xb2", "sg=="}, {1, "\xb3", "sw=="},
+    {1, "\xb4", "tA=="}, {1, "\xb5", "tQ=="}, {1, "\xb6", "tg=="},
+    {1, "\xb7", "tw=="}, {1, "\xb8", "uA=="}, {1, "\xb9", "uQ=="},
+    {1, "\xba", "ug=="}, {1, "\xbb", "uw=="}, {1, "\xbc", "vA=="},
+    {1, "\xbd", "vQ=="}, {1, "\xbe", "vg=="}, {1, "\xbf", "vw=="},
+    {1, "\xc0", "wA=="}, {1, "\xc1", "wQ=="}, {1, "\xc2", "wg=="},
+    {1, "\xc3", "ww=="}, {1, "\xc4", "xA=="}, {1, "\xc5", "xQ=="},
+    {1, "\xc6", "xg=="}, {1, "\xc7", "xw=="}, {1, "\xc8", "yA=="},
+    {1, "\xc9", "yQ=="}, {1, "\xca", "yg=="}, {1, "\xcb", "yw=="},
+    {1, "\xcc", "zA=="}, {1, "\xcd", "zQ=="}, {1, "\xce", "zg=="},
+    {1, "\xcf", "zw=="}, {1, "\xd0", "0A=="}, {1, "\xd1", "0Q=="},
+    {1, "\xd2", "0g=="}, {1, "\xd3", "0w=="}, {1, "\xd4", "1A=="},
+    {1, "\xd5", "1Q=="}, {1, "\xd6", "1g=="}, {1, "\xd7", "1w=="},
+    {1, "\xd8", "2A=="}, {1, "\xd9", "2Q=="}, {1, "\xda", "2g=="},
+    {1, "\xdb", "2w=="}, {1, "\xdc", "3A=="}, {1, "\xdd", "3Q=="},
+    {1, "\xde", "3g=="}, {1, "\xdf", "3w=="}, {1, "\xe0", "4A=="},
+    {1, "\xe1", "4Q=="}, {1, "\xe2", "4g=="}, {1, "\xe3", "4w=="},
+    {1, "\xe4", "5A=="}, {1, "\xe5", "5Q=="}, {1, "\xe6", "5g=="},
+    {1, "\xe7", "5w=="}, {1, "\xe8", "6A=="}, {1, "\xe9", "6Q=="},
+    {1, "\xea", "6g=="}, {1, "\xeb", "6w=="}, {1, "\xec", "7A=="},
+    {1, "\xed", "7Q=="}, {1, "\xee", "7g=="}, {1, "\xef", "7w=="},
+    {1, "\xf0", "8A=="}, {1, "\xf1", "8Q=="}, {1, "\xf2", "8g=="},
+    {1, "\xf3", "8w=="}, {1, "\xf4", "9A=="}, {1, "\xf5", "9Q=="},
+    {1, "\xf6", "9g=="}, {1, "\xf7", "9w=="}, {1, "\xf8", "+A=="},
+    {1, "\xf9", "+Q=="}, {1, "\xfa", "+g=="}, {1, "\xfb", "+w=="},
+    {1, "\xfc", "/A=="}, {1, "\xfd", "/Q=="}, {1, "\xfe", "/g=="},
+    {1, "\xff", "/w=="},
+};
+
+constexpr EncodedData kRandomTestData[] = {
+    {2, "\x63\xa9", "Y6k="},
+    {2, "\xa1\x49", "oUk="},
+    {2, "\x14\x58", "FFg="},
+    {2, "\x5d\xa2", "XaI="},
+    {2, "\x7c\x80", "fIA="},
+    {2, "\xc1\xbb", "wbs="},
+    {2, "\x08\x00", "CAA="},
+    {2, "\xd8\x88", "2Ig="},
+    {2, "\x74\x6d", "dG0="},
+    {2, "\x22\x86", "IoY="},
+    {3, "\x69\x89\x03", "aYkD"},
+    {3, "\x6c\xcb\xc5", "bMvF"},
+    {3, "\x72\x36\x8b", "cjaL"},
+    {3, "\xd3\xdc\xe0", "09zg"},
+    {3, "\x5d\x1f\x8a", "XR+K"},
+    {3, "\x0d\xc0\x5b", "DcBb"},
+    {3, "\xe3\x11\x1e", "4xEe"},
+    {3, "\xbc\x3c\xb9", "vDy5"},
+    {3, "\xc0\xa2\x1c", "wKIc"},
+    {3, "\xa9\x67\xfb", "qWf7"},
+    {4, "\x80\xf5\xc8\xd4", "gPXI1A=="},
+    {4, "\xa3\x54\x4a\xfa", "o1RK+g=="},
+    {4, "\x69\xdb\x14\x4c", "adsUTA=="},
+    {4, "\x95\x20\x23\x1a", "lSAjGg=="},
+    {4, "\xb9\x2c\x00\x11", "uSwAEQ=="},
+    {4, "\xef\xeb\x23\x44", "7+sjRA=="},
+    {4, "\xcf\xa9\xe6\x85", "z6nmhQ=="},
+    {4, "\xc5\xe0\x36\xde", "xeA23g=="},
+    {4, "\x77\xe1\x63\x51", "d+FjUQ=="},
+    {4, "\x7d\xa6\x8c\x5e", "faaMXg=="},
+    {5, "\x6e\xb8\x91\x3f\xac", "briRP6w="},
+    {5, "\xd1\x16\x7f\x1d\xef", "0RZ/He8="},
+    {5, "\x42\x95\xfb\x24\xee", "QpX7JO4="},
+    {5, "\x19\xfd\xe5\x96\xc1", "Gf3llsE="},
+    {5, "\x42\x5a\xb3\xfe\x13", "Qlqz/hM="},
+    {5, "\x2b\xf7\x1a\xcc\x13", "K/cazBM="},
+    {5, "\xba\x8f\x0d\xf7\xc1", "uo8N98E="},
+    {5, "\x28\xa6\x77\x2d\xfc", "KKZ3Lfw="},
+    {5, "\x68\xaa\x19\x59\xd0", "aKoZWdA="},
+    {5, "\x46\x73\xd3\x54\x7e", "RnPTVH4="},
+    {6, "\x1f\x88\x91\xbb\xd7\x10", "H4iRu9cQ"},
+    {6, "\x37\x23\x3b\x5a\x26\xe4", "NyM7Wibk"},
+    {6, "\xd2\xa0\xf4\x13\x91\xe6", "0qD0E5Hm"},
+    {6, "\x55\xe8\xe9\x06\x5d\xc3", "VejpBl3D"},
+    {6, "\xeb\xf5\xd8\x62\x3c\x5e", "6/XYYjxe"},
+    {6, "\xee\xad\x7e\xc4\x66\x83", "7q1+xGaD"},
+    {6, "\xbb\x07\x2c\x26\x3f\xb7", "uwcsJj+3"},
+    {6, "\xed\xf3\x34\x94\xab\x41", "7fM0lKtB"},
+    {6, "\x3f\xe8\x18\x4c\xe8\xf4", "P+gYTOj0"},
+    {6, "\x0a\xdd\x39\xbc\x1f\x65", "Ct05vB9l"},
+    {7, "\xac\xcf\xb2\xd5\xee\xa2\x8e", "rM+y1e6ijg=="},
+    {7, "\x78\x63\xeb\x3f\x07\xde\x04", "eGPrPwfeBA=="},
+    {7, "\x7a\xd7\x3b\x5c\x09\xc2\x93", "etc7XAnCkw=="},
+    {7, "\xd4\xe4\xda\xe3\xf3\x4d\xe9", "1OTa4/NN6Q=="},
+    {7, "\xa6\xc6\x7c\x47\xd5\xbe\xd3", "psZ8R9W+0w=="},
+    {7, "\x34\xad\x5d\x02\x47\xa1\x39", "NK1dAkehOQ=="},
+    {7, "\x33\x98\xd7\x02\x46\x4e\xad", "M5jXAkZOrQ=="},
+    {7, "\x08\x4d\x48\x48\xb1\x3d\x05", "CE1ISLE9BQ=="},
+    {7, "\xc4\x5e\x4a\x6d\x4a\x04\xb6", "xF5KbUoEtg=="},
+    {7, "\x12\xe9\xf4\xaa\x2e\x4c\x31", "Eun0qi5MMQ=="},
+    {8, "\xff\x15\x25\x7e\x7b\xc9\x7b\x60", "/xUlfnvJe2A="},
+    {8, "\xc7\xbb\x0b\x62\x5c\x62\x41\xc2", "x7sLYlxiQcI="},
+    {8, "\x48\x49\x6d\x7c\xca\xb7\xae\xed", "SEltfMq3ru0="},
+    {8, "\xfd\xec\x13\xd6\x93\x9f\xba\xe0", "/ewT1pOfuuA="},
+    {8, "\x7e\xff\xd2\xdd\x0e\xe2\x6c\x60", "fv/S3Q7ibGA="},
+    {8, "\xe5\xba\x41\x65\xa0\x46\x17\x27", "5bpBZaBGFyc="},
+    {8, "\xce\xec\xd5\x68\x3a\xb7\xb4\x16", "zuzVaDq3tBY="},
+    {8, "\xbe\x33\x9a\xc9\xfd\xcc\x29\xe8", "vjOayf3MKeg="},
+    {8, "\x55\x8c\x60\xcc\xc4\x7d\x99\x1f", "VYxgzMR9mR8="},
+    {8, "\xee\x21\x88\x2a\x0f\x7e\x76\xd7", "7iGIKg9+dtc="},
+    {9, "\xd5\xab\xd9\xa6\xae\xaa\x33\x9f\x66", "1avZpq6qM59m"},
+    {9, "\x6f\xe8\x06\xcf\xfd\x79\x3a\x4e\xdb", "b+gGz/15Ok7b"},
+    {9, "\x61\x00\x0a\x51\xad\x5b\xf1\xf9\x37", "YQAKUa1b8fk3"},
+    {9, "\x4f\x40\x0b\x79\x10\xa4\x12\x25\x3e", "T0ALeRCkEiU+"},
+    {9, "\xb1\x37\xb3\x41\x5b\xd7\xe8\xa4\xda", "sTezQVvX6KTa"},
+    {9, "\x82\xa5\x22\xd3\x48\xd8\xf7\x62\x7a", "gqUi00jY92J6"},
+    {9, "\xfd\x05\x33\x92\x2c\xd3\x85\x29\xa2", "/QUzkizThSmi"},
+    {9, "\x32\x93\x53\x06\x9c\xbb\x96\xbb\xf3", "MpNTBpy7lrvz"},
+    {9, "\xba\x40\x1d\x06\x92\xce\xc2\x8a\x28", "ukAdBpLOwooo"},
+    {9, "\xcc\x89\xf5\xeb\x49\x91\xa6\xa6\x88", "zIn160mRpqaI"},
+    {10, "\x6b\xfd\x95\xc5\x4a\xc7\xc2\x39\x45\xdc", "a/2VxUrHwjlF3A=="},
+    {10, "\x34\x50\xab\x78\xaf\x92\x47\x56\x8a\xb6", "NFCreK+SR1aKtg=="},
+    {10, "\x07\x14\x0a\xe8\x49\xca\x3a\x36\x80\xb0", "BxQK6EnKOjaAsA=="},
+    {10, "\xde\x79\x3d\xa7\xab\x22\xa9\xaa\xfc\x05", "3nk9p6siqar8BQ=="},
+    {10, "\x73\x62\x02\x77\x41\x91\xe6\x8b\x3f\x89", "c2ICd0GR5os/iQ=="},
+    {10, "\xf2\x09\xa9\x8b\x7c\x30\x26\x54\xf0\xd3", "8gmpi3wwJlTw0w=="},
+    {10, "\x32\xc8\xcc\xfc\x47\xa3\xac\x20\x37\x39", "MsjM/EejrCA3OQ=="},
+    {10, "\x32\x1b\x2b\x36\x07\x76\x90\xfa\xe0\x04", "MhsrNgd2kPrgBA=="},
+    {10, "\x55\x6b\x11\xe4\xc2\x22\xb0\x40\x14\x53", "VWsR5MIisEAUUw=="},
+    {10, "\xd3\x1e\xc4\xe5\x06\x60\x37\x51\x10\x48", "0x7E5QZgN1EQSA=="},
+    {11, "\x4c\xde\xee\xb8\x68\x0d\x9c\x66\x3e\xea\x46", "TN7uuGgNnGY+6kY="},
+    {11, "\x36\x79\x11\x5c\xce\xd0\xdf\x3c\xd2\xc9\x45", "NnkRXM7Q3zzSyUU="},
+    {11, "\xa0\x78\xc3\xc0\x79\xaf\xa1\xc3\xef\xd5\xf3", "oHjDwHmvocPv1fM="},
+    {11, "\xdd\x6b\x78\x18\x95\x80\x99\x7a\x02\x41\xe8", "3Wt4GJWAmXoCQeg="},
+    {11, "\x18\xfa\x19\xe0\xce\x3b\x0a\xa1\xec\x2b\x30", "GPoZ4M47CqHsKzA="},
+    {11, "\x74\xf2\x96\x90\x95\xbe\x14\x64\xbf\x10\xd9", "dPKWkJW+FGS/ENk="},
+    {11, "\x7f\xe8\x18\xab\xeb\x28\x86\xf1\x7c\x75\x47", "f+gYq+sohvF8dUc="},
+    {11, "\xa4\xc9\x62\x73\x0e\x89\xe1\x51\x8b\xf0\x96", "pMlicw6J4VGL8JY="},
+    {11, "\x98\xae\x09\x8c\x61\x40\xbf\x77\xde\xd9\x0d", "mK4JjGFAv3fe2Q0="},
+    {11, "\x86\x39\x06\xa1\xc6\xfc\xcf\x30\x21\xba\xdf", "hjkGocb8zzAhut8="},
+};
+
+void ExpectEncodeDecodeSizesMatch(const EncodedData& data) {
+  const size_t actual_encoded_size = std::strlen(data.encoded_data);
+  const size_t actual_raw_size = data.binary_size;
+
+  // Encoded size.
+  ASSERT_EQ(EncodedSize(data.binary_size), actual_encoded_size);
+  ASSERT_EQ(PW_BASE64_ENCODED_SIZE(data.binary_size), actual_encoded_size);
+
+  // Max decoded size. Do upper & lower bounds.
+  ASSERT_GE(MaxDecodedSize(actual_encoded_size), actual_raw_size);
+  ASSERT_GE(PW_BASE64_MAX_DECODED_SIZE(actual_encoded_size), actual_raw_size);
+  ASSERT_LE(MaxDecodedSize(actual_encoded_size), actual_raw_size + 2);
+  ASSERT_LE(PW_BASE64_MAX_DECODED_SIZE(actual_encoded_size),
+            actual_raw_size + 2);
+}
+
+// Tests both the C++ constexpr variant and the C macro variant.
+TEST(Base64, EncodedAndDecodedSize) {
+  for (const EncodedData& data : kSingleCharTestData) {
+    ExpectEncodeDecodeSizesMatch(data);
+  }
+  for (const EncodedData& data : kRandomTestData) {
+    ExpectEncodeDecodeSizesMatch(data);
+  }
+}
+
+TEST(Base64, Encode_SingleChar) {
+  char output[32];
+  for (const EncodedData& data : kSingleCharTestData) {
+    const size_t size = EncodedSize(data.binary_size);
+    ASSERT_EQ(std::strlen(data.encoded_data), size);
+    Encode(as_bytes(span(data.binary_data, data.binary_size)), output);
+    output[size] = '\0';
+    EXPECT_STREQ(data.encoded_data, output);
+  }
+}
+
+TEST(Base64, Encode_RandomData) {
+  char output[128];
+  for (const EncodedData& data : kRandomTestData) {
+    const size_t size = EncodedSize(data.binary_size);
+    ASSERT_EQ(std::strlen(data.encoded_data), size);
+    Encode(as_bytes(span(data.binary_data, data.binary_size)), output);
+    output[size] = '\0';
+    EXPECT_STREQ(data.encoded_data, output);
+  }
+}
+
+TEST(Base64, Encode_BoundaryCheck) {
+  constexpr std::byte data[] = {std::byte{'h'}, std::byte{'i'}};
+  char output[5] = {};
+
+  EXPECT_EQ(0u, Encode(data, span(output, 3)));
+  EXPECT_STREQ("", output);
+  EXPECT_EQ(4u, Encode(data, span(output, 4)));
+  EXPECT_STREQ("aGk=", output);
+}
+
+TEST(Base64, Decode_SingleChar) {
+  char output[32];
+  for (const EncodedData& data : kSingleCharTestData) {
+    size_t binary_size = Decode(data.encoded_data, output);
+    ASSERT_EQ(binary_size, data.binary_size);
+    EXPECT_EQ(0, std::memcmp(data.binary_data, output, data.binary_size));
+  }
+}
+
+TEST(Base64, Decode_RandomData) {
+  char output[128];
+  for (const EncodedData& data : kRandomTestData) {
+    size_t binary_size = Decode(data.encoded_data, output);
+    ASSERT_EQ(binary_size, data.binary_size);
+    EXPECT_EQ(0, std::memcmp(data.binary_data, output, data.binary_size));
+  }
+}
+
+TEST(Base64, Decode_BoundaryCheck) {
+  constexpr const char encoded_data[] = "aGk=";
+  std::byte output[4] = {};
+
+  EXPECT_EQ(0u, Decode(encoded_data, span(output, 2)));
+  EXPECT_STREQ("", reinterpret_cast<const char*>(output));
+  EXPECT_EQ(2u, Decode(encoded_data, span(output, 3)));
+  EXPECT_STREQ("hi", reinterpret_cast<const char*>(output));
+}
+
+TEST(Base64, Decode_InPlace) {
+  constexpr const char expected[] = "This is a secret message";
+  char buf[] = "VGhpcyBpcyBhIHNlY3JldCBtZXNzYWdl";
+  EXPECT_EQ(sizeof(expected) - 1, Decode(buf, buf));
+  EXPECT_EQ(0, std::memcmp(expected, buf, sizeof(expected) - 1));
+}
+
+TEST(Base64, Decode_UrlSafeDecode) {
+  char output[9] = {};
+
+  EXPECT_TRUE(IsValid("+f//WW8h"));
+  EXPECT_TRUE(IsValid("-f__WW8h"));
+
+  EXPECT_EQ(6u, Decode("-f__WW8h", output));
+  EXPECT_STREQ("\xf9\xff\xffYo!", output);
+}
+
+TEST(Base64, Empty) {
+  char buffer[] = "DO NOT TOUCH";
+  EXPECT_EQ(0u, EncodedSize(0));
+  Encode(as_bytes(span("Something cool!!!", 0)), buffer);
+  EXPECT_STREQ("DO NOT TOUCH", buffer);
+
+  EXPECT_EQ(0u, MaxDecodedSize(0));
+  EXPECT_EQ(0u, Decode(std::string_view("nothing please", 0), buffer));
+  EXPECT_STREQ("DO NOT TOUCH", buffer);
+}
+
+TEST(Base64, ExampleFromRfc3548Section7) {
+  constexpr uint8_t input[] = {0x14, 0xfb, 0x9c, 0x03, 0xd9, 0x7e};
+  char output[EncodedSize(sizeof(input)) + 1] = {};
+
+  Encode(as_bytes(span(input)), output);
+  EXPECT_STREQ("FPucA9l+", output);
+  Encode(as_bytes(span(input, 5)), output);
+  EXPECT_STREQ("FPucA9k=", output);
+  Encode(as_bytes(span(input, 4)), output);
+  EXPECT_STREQ("FPucAw==", output);
+
+  EXPECT_EQ(6u, Decode("FPucA9l+", output));
+  EXPECT_EQ(0, std::memcmp(input, output, 6));
+  EXPECT_EQ(5u, Decode("FPucA9k=", output));
+  EXPECT_EQ(0, std::memcmp(input, output, 5));
+  EXPECT_EQ(4u, Decode("FPucAw==", output));
+  EXPECT_EQ(0, std::memcmp(input, output, 4));
+}
+
+TEST(Base64, ExampleFromRfc4648Section9) {
+  char output[EncodedSize(sizeof("foobar")) + 1] = {};
+  const std::byte* foobar = reinterpret_cast<const std::byte*>("foobar");
+
+  Encode(span(foobar, 0), output);
+  EXPECT_STREQ("", output);
+  Encode(span(foobar, 1), output);
+  EXPECT_STREQ("Zg==", output);
+  Encode(span(foobar, 2), output);
+  EXPECT_STREQ("Zm8=", output);
+  Encode(span(foobar, 3), output);
+  EXPECT_STREQ("Zm9v", output);
+  Encode(span(foobar, 4), output);
+  EXPECT_STREQ("Zm9vYg==", output);
+  Encode(span(foobar, 5), output);
+  EXPECT_STREQ("Zm9vYmE=", output);
+  Encode(span(foobar, 6), output);
+  EXPECT_STREQ("Zm9vYmFy", output);
+
+  std::memset(output, '\0', sizeof(output));
+  EXPECT_EQ(0u, Decode("", output));
+  EXPECT_STREQ("", output);
+  EXPECT_EQ(1u, Decode("Zg==", output));
+  EXPECT_STREQ("f", output);
+  EXPECT_EQ(2u, Decode("Zm8=", output));
+  EXPECT_STREQ("fo", output);
+  EXPECT_EQ(3u, Decode("Zm9v", output));
+  EXPECT_STREQ("foo", output);
+  EXPECT_EQ(4u, Decode("Zm9vYg==", output));
+  EXPECT_STREQ("foob", output);
+  EXPECT_EQ(5u, Decode("Zm9vYmE=", output));
+  EXPECT_STREQ("fooba", output);
+  EXPECT_EQ(6u, Decode("Zm9vYmFy", output));
+  EXPECT_STREQ("foobar", output);
+}
+
+// Functions that call the Base64 API from C. These are defined in
+// base64_test.c; no point in having a separate header.
+extern "C" {
+
+void pw_Base64CallEncode(const void* binary_data,
+                         const size_t binary_size_bytes,
+                         char* output);
+
+size_t pw_Base64CallDecode(const char* base64,
+                           size_t base64_size_bytes,
+                           void* output);
+
+bool pw_Base64CallIsValid(const char* base64_data, size_t base64_size);
+
+}  // extern "C"
+
+constexpr const char kBase64[] = "aaaabbbbcc#%";
+
+// Ensure that the C API works correctly from a C-only context.
+TEST(Base64, IsValid_Ok) {
+  EXPECT_TRUE(IsValid(std::string_view(kBase64, 4)));
+  EXPECT_TRUE(IsValid(std::string_view(kBase64, 8)));
+}
+
+TEST(Base64, IsValid_IncorrectSize) {
+  EXPECT_FALSE(IsValid(std::string_view(kBase64, 5)));
+  EXPECT_FALSE(IsValid(std::string_view(kBase64, 6)));
+  EXPECT_FALSE(IsValid(std::string_view(kBase64, 7)));
+  EXPECT_FALSE(IsValid(std::string_view(kBase64, 10)));
+}
+
+TEST(Base64, IsValid_InvalidCharacters) {
+  EXPECT_FALSE(IsValid(std::string_view(kBase64, 11)));
+  EXPECT_FALSE(IsValid(std::string_view(kBase64, 12)));
+}
+
+TEST(Base64CLinkage, IsValid_Ok) {
+  EXPECT_TRUE(pw_Base64CallIsValid(kBase64, 4));
+  EXPECT_TRUE(pw_Base64CallIsValid(kBase64, 8));
+}
+
+TEST(Base64CLinkage, IsValid_IncorrectSize) {
+  EXPECT_FALSE(pw_Base64CallIsValid(kBase64, 5));
+  EXPECT_FALSE(pw_Base64CallIsValid(kBase64, 6));
+  EXPECT_FALSE(pw_Base64CallIsValid(kBase64, 7));
+  EXPECT_FALSE(pw_Base64CallIsValid(kBase64, 10));
+}
+
+TEST(Base64CLinkage, IsValid_InvalidCharacters) {
+  EXPECT_FALSE(pw_Base64CallIsValid(kBase64, 11));
+  EXPECT_FALSE(pw_Base64CallIsValid(kBase64, 12));
+}
+
+TEST(Base64CLinkage, Encode) {
+  char output[EncodedSize(sizeof("foobar")) + 1] = {};
+
+  pw_Base64CallEncode("", 0, output);
+  EXPECT_STREQ("", output);
+  pw_Base64CallEncode("f", 1, output);
+  EXPECT_STREQ("Zg==", output);
+  pw_Base64CallEncode("fo", 2, output);
+}
+
+TEST(Base64CLinkage, Decode) {
+  char output[EncodedSize(sizeof("foobar")) + 1] = {};
+
+  EXPECT_EQ(0u, pw_Base64CallDecode("", 0, output));
+  EXPECT_STREQ("", output);
+  EXPECT_EQ(1u, pw_Base64CallDecode("Zg==", 4, output));
+  EXPECT_STREQ("f", output);
+  EXPECT_EQ(2u, pw_Base64CallDecode("Zm8=", 4, output));
+  EXPECT_STREQ("fo", output);
+}
+
+}  // namespace
+}  // namespace pw::base64
diff --git a/pw_base64/public/pw_base64/base64.h b/pw_base64/public/pw_base64/base64.h
new file mode 100644
index 0000000..24c425e
--- /dev/null
+++ b/pw_base64/public/pw_base64/base64.h
@@ -0,0 +1,130 @@
+// Copyright 2020 The Pigweed Authors
+//
+// 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
+//
+//     https://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.
+
+// Functions for encoding and decoding data in Base64 as specified by RFC 3548
+// and RFC 4648. See https://tools.ietf.org/html/rfc4648
+#pragma once
+
+#include <stdbool.h>
+#include <stddef.h>
+
+// C-compatible versions of a subset of the pw_base64 module.
+#ifdef __cplusplus
+extern "C" {
+#endif  // __cplusplus
+
+// Returns the size of the given number of bytes when encoded as Base64. Base64
+//
+// Equivalent to pw::base64::EncodedSize().
+#define PW_BASE64_ENCODED_SIZE(binary_size_bytes) \
+  (((size_t)binary_size_bytes + 2) / 3 * 4)  // +2 to round up to a 3-byte group
+
+// Encodes the provided data in Base64 and writes the result to the buffer.
+// Exactly PW_BASE64_ENCODED_SIZE(binary_size_bytes) bytes will be written. The
+// output buffer *MUST* be large enough for the encoded output!
+//
+// Equivalent to pw::base64::Encode().
+void pw_Base64Encode(const void* binary_data,
+                     const size_t binary_size_bytes,
+                     char* output);
+
+// Evaluates to the maximum size of decoded Base64 data in bytes.
+//
+// Equivalent to pw::base64::MaxDecodedSize().
+#define PW_BASE64_MAX_DECODED_SIZE(base64_size_bytes) \
+  (((size_t)base64_size_bytes) / 4 * 3)
+
+// Decodes the provided Base64 data into raw binary. The output buffer *MUST* be
+// at least PW_BASE64_MAX_DECODED_SIZE bytes large.
+//
+// Equivalent to pw::base64::Decode().
+size_t pw_Base64Decode(const char* base64,
+                       size_t base64_size_bytes,
+                       void* output);
+
+// Returns true if the provided string is valid Base64 encoded data. Accepts
+// either the standard (+/) or URL-safe (-_) alphabets.
+//
+// Equivalent to pw::base64::IsValid().
+bool pw_Base64IsValid(const char* base64_data, size_t base64_size);
+
+// C++ API, which uses the C functions internally.
+#ifdef __cplusplus
+}  // extern "C"
+
+#include <string_view>
+#include <type_traits>
+
+#include "pw_span/span.h"
+
+namespace pw::base64 {
+
+// Returns the size of the given number of bytes when encoded as Base64. Base64
+// encodes 3-byte groups into 4-character strings. The final group is padded to
+// be 3-bytes if it only has 1 or 2.
+constexpr size_t EncodedSize(size_t binary_size_bytes) {
+  return PW_BASE64_ENCODED_SIZE(binary_size_bytes);
+}
+
+// Encodes the provided data in Base64 and writes the result to the buffer.
+// Encodes to the standard alphabet with + and / for characters 62 and 63.
+// Exactly EncodedSize(binary_size_bytes) bytes will be written. The
+// output buffer *MUST* be large enough for the encoded output! The input and
+// output buffers MUST NOT be the same; encoding cannot occur in place.
+//
+// The resulting string in the output is NOT null-terminated!
+inline void Encode(span<const std::byte> binary, char* output) {
+  pw_Base64Encode(binary.data(), binary.size_bytes(), output);
+}
+
+// Encodes the provided data in Base64 if the result fits in the provided
+// buffer. Returns the number of bytes written, which will be 0 if the output
+// buffer is too small.
+size_t Encode(span<const std::byte> binary, span<char> output_buffer);
+
+// Returns the maximum size of decoded Base64 data in bytes. base64_size_bytes
+// must be a multiple of 4, since Base64 encodes 3-byte groups into 4-character
+// strings. If the last 3-byte group has padding, the actual decoded size would
+// be 1 or 2 bytes less than MaxDecodedSize.
+constexpr size_t MaxDecodedSize(size_t base64_size_bytes) {
+  return PW_BASE64_MAX_DECODED_SIZE(base64_size_bytes);
+}
+
+// Decodes the provided Base64 data into raw binary. The output buffer *MUST* be
+// at least MaxDecodedSize bytes large. The output buffer may be the same as the
+// input buffer; decoding can occur in place. Returns the number of bytes that
+// were decoded.
+//
+// Decodes either standard (+/) or URL-safe (-_) alphabets. The data must be
+// padded to 4-character blocks with =. This function does NOT check that the
+// input is valid! Use IsValid or the four-argument overload to check the
+// input formatting.
+inline size_t Decode(std::string_view base64, void* output) {
+  return pw_Base64Decode(base64.data(), base64.size(), output);
+}
+
+// Decodes the provided Base64 data, if the data is valid and fits in the output
+// buffer. Returns the number of bytes written, which will be 0 if the data is
+// invalid or doesn't fit.
+size_t Decode(std::string_view base64, span<std::byte> output_buffer);
+
+// Returns true if the provided string is valid Base64 encoded data. Accepts
+// either the standard (+/) or URL-safe (-_) alphabets.
+inline bool IsValid(std::string_view base64) {
+  return pw_Base64IsValid(base64.data(), base64.size());
+}
+
+}  // namespace pw::base64
+
+#endif  // __cplusplus