Headers
I don't see any need for <string.h>
. We can drop <stdbool.h>
if we rewrite check_not_equal
:
#define check_not_equal(a, b) ((a) != (b))
Personally, I wouldn't use a macro for that - it just obfuscates the code.
<stdio.h>
is used only by the tests. We may compile them separately if we want to use the function in other programs.
And we can eliminate the need for <math.h>
using a well-known technique for integer ceiling divide:
int ceil_div(int n, int m) {
/* assumes n+m-1 doesn't overflow */
/* A bulletproof version is only slightly more complex. */
return (n+m-1) / m;
}
That's likely more efficient than using floating-point, too.
Macros
Don't use macros where functions would work equally well or better. In particular, we have lots of macros here that expand their arguments multiple times, which can cause surprise when they have side-effects.
Always use all-caps names for macros, as a warning to the reader that they are not functions.
Using functions is the right thing here - any decent compiler will inline for you when that produces better code.
And what's the use of the ternary expression like (((a < 52) ? 1 : 0) == 1)
? That's simply (a < 52)
.
It's simpler, clearer and more efficient just to index a lookup table for each of those:
/* a in "0123456789abcdef" */
static int from_hex(char c)
{
switch (c) {
default: return c - '0'; /* digits are guaranteed consecutive */*/
case 'a': case 'A': return 10;
case 'b': case 'B': return 11;
case 'c': case 'C': return 12;
case 'd': case 'D': return 13;
case 'e': case 'E': return 14;
case 'f': case 'F': return 15;
}
}
/* 0 <= a <= 63 */
static char to_base64(int a)
{
return "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/+"[a];
}
Or just declare the strings, and let the code do the indexing.
Don't use fixed-size types unnecessarily
uint8_t
isn't available everywhere (notably, whenever CHAR_BIT
is greater than 8). But we don't exactly 8 bits - we can use uint8_fast_t
or simply unsigned char
for our purposes.
Character set assumptions
The code seems to assume that the output characters are ASCII-encoded (which includes Latin-1 and UTF-8, but not EBCDIC). Worse than that, it uses decimal code-point numbers (e.g. 65
) where character constants ('a'
) would be easier to understand.
Don't assume allocation succeeds
malloc()
can return a null pointer. Dereferencing a null pointer is undefined behaviour, so just report that and exit early; the usual way is to return a null pointer to mean "could not allocate":
char *base64 = malloc(n);
if (!base64) { return base64; }
Note that multiplying by sizeof (char)
is pointless, since that's 1 by definition (sizeof
yields the number of char
needed to represent a type).
Control flow
This structure can be simplified:
if (size++ != n) {
c = (mask1 & hex_array[i]) >> 2;
base64[temp * 4] = return_base64(c);
} else
break;
It's good practice to use braces {
...}
even for single-statement blocks, and especially to be consistent both sides of else
. But we don't need else
here if we invert the test:
if (size++ == n) {
break;
}
c = (mask1 & hex_array[i]) >> 2;
base64[temp * 4] = return_base64(c);
Test program
It's good that we have tests. We can make it better by having self-checking tests instead of needing to inspect program output.
We could write that with a couple of helper functions:
#include <stdio.h>
static unsigned test_base64_char(const char *file, int line,
int n, char expected)
{
char actual = return_base64(n);
if (actual == expected) {
return 0;
}
fprintf(stderr, "%s:%d: return_base64(%d) returned '%c'; expected '%c'\n",
file, line, n, actual, expected);
return 1;
}
static unsigned test_base64_str(const char *file, int line,
const char *input, const char *expected)
{
char *actual = convert_to_base64(input);
if (!actual) {
fprintf(stderr, "%s:%d: return_base64(\"%s\") returned null; expected \"%s\"\n",
file, line, input, expected);
return 1;
}
if (!strcmp(actual, expected)) {
free(actual);
return 0;
}
fprintf(stderr, "%s:%d: return_base64(\"%s\") returned \"%s\"; expected \"%s\"\n",
file, line, input, actual, expected);
free(actual);
return 1;
}
Some macros to add the source location of the tests:
#define TEST_BASE64_CHAR(n, expected) \
failures += test_base64_char(__FILE__, __LINE__, n, expected)
#define TEST_BASE64_STR(n, expected) \
failures += test_base64_str(__FILE__, __LINE__, n, expected)
And a main()
function to run them:
int main(void)
{
unsigned failures = 0;
TEST_BASE64_CHAR(4, 'E');
TEST_BASE64_CHAR(19, 'T');
TEST_BASE64_CHAR(37, 'l');
TEST_BASE64_CHAR(49, 'x');
TEST_BASE64_CHAR(54, '2');
TEST_BASE64_CHAR(59, '7');
TEST_BASE64_CHAR(62, '+');
TEST_BASE64_CHAR(63, '/');
TEST_BASE64_STR("deadbeef", "3q2+7w");
TEST_BASE64_STR("49276d206b696c6c696e6720796f757220627261696e206c696b65206120706f69736f6e6f7573206d757368726f6f6d",
"SSdtIGtpbGxpbmcgeW91ciBicmFpbiBsaWtlIGEgcG9pc29ub3VzIG11c2hyb29t");
printf("Tests completed with %u failures.\n", failures);
return failures > 0;
}
Temporary array
The code works in two phases, reflected in the two functions, with a helper array allocated to hold the binary representation of the hex string. But that helper is unnecessary - we should be able to work directly from the hex chars, converting each group of three into two base-64 chars.
/*
Function to convert a hexadecimal-encoded string to a base64-encoded string.
params : hexadecimal-encoded string
returns : string - or a null pointer if allocation failed
The return value must be deallocated using free()
*/
char *convert_to_base64(const char *hex_string)
{
size_t input_len = strlen(hex_string);
size_t output_len = (input_len * 2 + 2) / 3 + 1;
char *const out_buf = malloc(output_len);
if (!out_buf) {
return NULL;
}
char *out = out_buf;
while (hex_string[0] && hex_string[1] && hex_string[2]) {
/* convert three hex digits to two base-64 chars */
int digit[3] = {
from_hex(hex_string[0]),
from_hex(hex_string[1]),
from_hex(hex_string[2])
};
*out++ = to_base64((digit[0] << 2) + (digit[1] >> 2));
*out++ = to_base64(((digit[1] & 3) << 4) + (digit[2]));
hex_string += 3;
}
/* Now deal with leftover chars */
if (hex_string[0] && hex_string[1]) {
/* convert three hex digits to two base-64 chars */
int digit[2] = {
from_hex(hex_string[0]),
from_hex(hex_string[1])
};
*out++ = to_base64((digit[0] << 2) + (digit[1] >> 2));
*out++ = to_base64(((digit[1] & 3) << 4));
} else if (hex_string[0]) {
/* convert three hex digits to two base-64 chars */
int digit = from_hex(hex_string[0]);
*out++ = to_base64(digit << 2);
}
*out = '\0';
return out_buf;
}
Output format ambiguity
There's no indication of how many blank characters were encoded, unlike standard base-64 formats, which use one or two trailing =
to indicate this. That will break round-trip of inputs such as "1d"
, which is indistinguishable from "1d0"
after encoding (both produce `"HQ" as output).
Modified code
Header file hex_to_base64.h
:
#ifndef HEX_TO_BASE64_H
#define HEX_TO_BASE64_H
/*
Function to convert a hexadecimal-encoded string to a base64-encoded string.
params : hexadecimal-encoded string
returns : string - or a null pointer if allocation failed
The return value must be deallocated using free().
*/
char *hex_to_base64(const char *hex_string);
#endif
Implementation:
#include "hex_to_base64.h"
#include <stdlib.h>
#include <string.h>
/* a in "0123456789abcdef" */
static int from_hex(char c)
{
switch (c) {
default: return c - '0'; /* digits are guaranteed consecutive */
case 'a': case 'A': return 10;
case 'b': case 'B': return 11;
case 'c': case 'C': return 12;
case 'd': case 'D': return 13;
case 'e': case 'E': return 14;
case 'f': case 'F': return 15;
}
}
char *hex_to_base64(const char *hex_string)
{
static const char base64[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
size_t input_len = strlen(hex_string);
size_t output_len = (input_len * 2 + 2) / 3 /* base64 chars */
+ 2 - (input_len - 1) % 3 /* padding '=' or '==' */
+ 1; /* string terminator */
char *const out_buf = malloc(output_len);
if (!out_buf) {
return NULL;
}
char *out = out_buf;
while (hex_string[0] && hex_string[1] && hex_string[2]) {
/* convert three hex digits to two base-64 chars */
int digit[3] = {
from_hex(hex_string[0]),
from_hex(hex_string[1]),
from_hex(hex_string[2])
};
*out++ = base64[(digit[0] << 2) + (digit[1] >> 2)];
*out++ = base64[((digit[1] & 3) << 4) + (digit[2])];
hex_string += 3;
}
/* Now deal with leftover chars */
if (hex_string[0] && hex_string[1]) {
/* convert two hex digits to two base-64 chars */
int digit[2] = {
from_hex(hex_string[0]),
from_hex(hex_string[1])
};
*out++ = base64[(digit[0] << 2) + (digit[1] >> 2)];
*out++ = base64[((digit[1] & 3) << 4)];
*out++ = '=';
} else if (hex_string[0]) {
/* convert one hex digit to one base-64 char */
int digit = from_hex(hex_string[0]);
*out++ = base64[digit << 2];
*out++ = '=';
*out++ = '=';
}
*out = '\0';
return out_buf;
}
Tests:
#include <stdio.h>
static unsigned test_base64_str(const char *file, int line,
const char *input, const char *expected)
{
char *actual = hex_to_base64(input);
if (!actual) {
fprintf(stderr, "%s:%d: to_base64(\"%s\") returned null; expected \"%s\"\n",
file, line, input, expected);
return 1;
}
if (!strcmp(actual, expected)) {
free(actual);
return 0;
}
fprintf(stderr, "%s:%d: to_base64(\"%s\") returned \"%s\"; expected \"%s\"\n",
file, line, input, actual, expected);
free(actual);
return 1;
}
#define TEST_BASE64_STR(n, expected) \
failures += test_base64_str(__FILE__, __LINE__, n, expected)
int main(void)
{
unsigned failures = 0;
TEST_BASE64_STR("1d", "HQ=");
TEST_BASE64_STR("1d0", "HQ");
TEST_BASE64_STR("deadbeef", "3q2+7w=");
TEST_BASE64_STR("49276d206b696c6c696e6720796f757220627261696e206c696b65206120706f69736f6e6f7573206d757368726f6f6d",
"SSdtIGtpbGxpbmcgeW91ciBicmFpbiBsaWtlIGEgcG9pc29ub3VzIG11c2hyb29t");
printf("Tests completed with %u failures.\n", failures);
return failures > 0;
}
Use standard library for hex → integer conversion
We could use standard-library sscanf()
with a format string of %3x
to read the groups of three hex digits instead of coding our own from_hex()
. It can also tell us (using %n
) how many characters were converted. That gives a simpler implementation:
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char *hex_to_base64(const char *hex_string)
{
static const char base64[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
size_t input_len = strlen(hex_string);
size_t output_len = (input_len * 2 + 2) / 3 /* base64 chars */
+ 2 - (input_len - 1) % 3 /* padding '=' or '==' */
+ 1; /* string terminator */
char *const out_buf = malloc(output_len);
if (!out_buf) {
return out_buf;
}
unsigned int digits;
int d_len;
char *out = out_buf;
while (*hex_string) {
if (sscanf(hex_string, "%3x%n", &digits, &d_len) != 1) {
/* parse error */
free(out_buf);
return NULL;
}
switch (d_len) {
case 3:
*out++ = base64[digits >> 6];
*out++ = base64[digits & 0x3f];
break;
case 2:
digits <<= 4;
*out++ = base64[digits >> 6];
*out++ = base64[digits & 0x3f];
*out++ = '=';
break;
case 1:
*out++ = base64[digits];
*out++ = '=';
*out++ = '=';
}
hex_string += d_len;
}
*out++ = '\0';
assert(out == out_buf + output_len);
return out_buf;
}
'a'
not 97. Perhaps email me tobasile@starynkevitch.net
for more \$\endgroup\$