I wrote a program that converts a hex-encoded string to base64. It's my solution to the first of the Cryptopals challenges.
My main concerns are:
- Portability. I don't want to rely on implementation-specific or non-standard behavior.
- Reusability. I wrote a function to get bytes from hex and one to get base64 from bytes (rather than going straight from hex to base64), because I will probably need them for other challenges.
- Readability.
- Testability.
/* Convert hex to base64.
*
* This program takes as an argument a hex string, which it converts to base64
* and then prints to stdout.
*/
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* A bytestring represents a string of (8-bit) bytes.
*
* Even if CHAR_BIT > 8, byte values are always in the range [0, 255].
*/
typedef struct {
size_t size;
unsigned char *data;
} bytestring;
static const char *const HEX_DIGITS = "0123456789abcdef";
static const char *const BASE64_DIGITS =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
static const char BASE64_PADDING = '=';
/* Print a usage message to stderr.
*/
static void print_usage(int argc, const char *argv[])
{
const char *name = "s1e1.exe";
if (argc >= 1) {
name = argv[0];
}
fprintf(stderr, "Usage: %s hex\n", name);
}
/* Return the value represented by a hexadecimal digit.
*
* `digit` must be a valid hexadecimal digit.
*/
static unsigned char hex_value(char digit)
{
return (unsigned char) (strchr(HEX_DIGITS, digit) - HEX_DIGITS);
}
/* Convert a hex string to a bytestring and return true on success.
*
* On success, the bytestring data must be freed by the caller.
*/
static bool bytes_from_hex(const char *hex, bytestring *bytes_ptr)
{
size_t num_digits = 0;
for (const char *ptr = hex; *ptr != '\0'; ptr++) {
if (strchr(HEX_DIGITS, *ptr) == NULL) {
fprintf(stderr, "Invalid hex digit: %c\n", *ptr);
return false;
}
num_digits++;
}
if (num_digits % 2 == 1) {
fprintf(stderr, "Error: odd number of hex digits\n");
return false;
}
bytes_ptr->size = num_digits / 2;
if ((bytes_ptr->data = malloc(bytes_ptr->size)) == NULL) {
fprintf(stderr, "Error: couldn't allocate memory for bytes\n");
return false;
}
for (size_t i = 0; i < bytes_ptr->size; i++) {
unsigned char value_a = hex_value(hex[2*i + 0]);
unsigned char value_b = hex_value(hex[2*i + 1]);
bytes_ptr->data[i] = (unsigned char) (value_a << 4 | value_b);
}
return true;
}
/* Convert a bytestring to base64 and return true on success.
*
* On success, the base64 string must be freed by the caller.
*/
static bool base64_from_bytes(bytestring bytes, char **base64_ptr)
{
size_t num_triplets = (bytes.size + 2) / 3;
size_t buffer_size = 4 * num_triplets + 1;
if ((*base64_ptr = malloc(buffer_size)) == NULL) {
fprintf(stderr, "Error: couldn't allocate memory for base64\n");
return false;
}
for (size_t i = 0; i < num_triplets; i++) {
uint_fast32_t byte_a = bytes.data[3*i + 0];
uint_fast32_t byte_b = 3*i + 1 < bytes.size ? bytes.data[3*i + 1] : 0;
uint_fast32_t byte_c = 3*i + 2 < bytes.size ? bytes.data[3*i + 2] : 0;
uint_fast32_t value = byte_a << 16 | byte_b << 8 | byte_c;
unsigned char mask = (1 << 6) - 1;
(*base64_ptr)[4*i + 0] = BASE64_DIGITS[value >> 18 & mask];
(*base64_ptr)[4*i + 1] = BASE64_DIGITS[value >> 12 & mask];
(*base64_ptr)[4*i + 2] = BASE64_DIGITS[value >> 6 & mask];
(*base64_ptr)[4*i + 3] = BASE64_DIGITS[value >> 0 & mask];
if (3*i + 1 >= bytes.size) {
(*base64_ptr)[4*i + 2] = BASE64_PADDING;
}
if (3*i + 2 >= bytes.size) {
(*base64_ptr)[4*i + 3] = BASE64_PADDING;
}
}
(*base64_ptr)[buffer_size - 1] = '\0';
return true;
}
/* Convert a hex string to base64 and return true on success.
*
* On success, the base64 string must be freed by the caller.
*/
static bool base64_from_hex(const char *hex, char **base64_ptr)
{
bytestring bytes;
if (!bytes_from_hex(hex, &bytes)) {
return false;
}
bool success = base64_from_bytes(bytes, base64_ptr);
free(bytes.data);
return success;
}
/* Print the base64 representation of a hex string and return true on success.
*/
static bool print_base64(const char *hex)
{
char *base64;
if (!base64_from_hex(hex, &base64)) {
return false;
}
bool success = puts(base64) != EOF;
free(base64);
return success;
}
/* Print the base64 representation of the hex string given as an argument.
*/
int main(int argc, const char *argv[])
{
if (argc != 2) {
print_usage(argc, argv);
return EXIT_FAILURE;
}
if (!print_base64(argv[1])) {
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
BASE64_DIGITS
instead of at the very beginning? It makes more sense to me to represent base 64 as "hex and then some". \$\endgroup\$ – Lundin Sep 9 '19 at 9:13