A third follow up to the simple compression implementation. Adopted most of @holroy's suggestions. Main changes:
- Compression now uses high-bit to indicate that additional length bytes are still following
- Added decompression support
- Should no longer depend on
CHAR_BITS == 8
which increases portability - Use of
unsigned long long
since exact type size is not really important - Added additional options for printing command line help and decompression
- Options are no longer global, instead an
options
object is being passed around
And here comes the code:
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <strings.h>
#include <limits.h>
#define BIT_SHIFT (CHAR_BIT - 1)
const int HIGH_BIT = 1 << BIT_SHIFT;
const int LOWER_BITS = ~(~0 << BIT_SHIFT);
typedef struct
{
bool print_hex : 1;
bool decompress : 1;
bool print_help : 1;
} options;
void write_char(int c, options *opts)
{
if (printf(opts->print_hex ? "0x%x " : "%c", c) < 0)
{
perror("Error printing to stdout.");
exit(EXIT_FAILURE);
}
}
void write_count(unsigned long long count, options *opts)
{
int lower_bits = count & LOWER_BITS;
while (lower_bits != count || count > LOWER_BITS)
{
write_char(lower_bits | HIGH_BIT, opts);
count >>= BIT_SHIFT;
lower_bits = count & LOWER_BITS;
}
write_char(lower_bits, opts);
}
bool is_one_of(const char *value, const char *short_option, const char *long_option)
{
return (strcasecmp(value, short_option) == 0 || strcasecmp(value, long_option) == 0);
}
options parse_args(int argc, char **argv)
{
options opts = { 0 };
for (int i = 1; i < argc; ++i)
{
if (is_one_of(argv[i], "-x", "--hex"))
{
opts.print_hex = true;
}
else if (is_one_of(argv[i], "-h", "--help"))
{
opts.print_help = true;
}
else if (is_one_of(argv[i], "-e", "--decompress"))
{
opts.decompress = true;
}
else
{
fprintf(stderr, "Invalid option %s\n", argv[i]);
opts.print_help = true;
}
}
return opts;
}
void compress(options *opts)
{
int current_char = 0;
int previous_char = 0;
unsigned long long current_char_count = 0;
while (EOF != (current_char = getchar()))
{
if (current_char_count > 0 && current_char_count < ULLONG_MAX && current_char == previous_char)
{
current_char_count += 1;
}
else
{
if (current_char_count > 0)
{
write_count(current_char_count, opts);
}
write_char(current_char, opts);
current_char_count = 1;
}
previous_char = current_char;
}
// write remainder
if (current_char_count > 0)
{
write_count(current_char_count, opts);
}
}
unsigned long long read_length()
{
unsigned long long length = 0;
bool expect_more_length_bytes = true;
while (expect_more_length_bytes)
{
int c = getchar();
if (c == EOF)
{
fprintf(stderr, "unexpected end of stream, expected more length bytes\n");
exit(EXIT_FAILURE);
}
expect_more_length_bytes = (c & HIGH_BIT) != 0;
length <<= BIT_SHIFT;
length |= (c & LOWER_BITS);
}
return length;
}
void decompress(options *opts)
{
int current_char = 0;
while (EOF != (current_char = getchar()))
{
unsigned long long length = read_length();
for (unsigned long long i = 0; i < length; ++i)
{
write_char(current_char, opts);
}
}
}
void print_help(char *program_name)
{
printf("Usage: %s [options]\n", program_name);
puts("Options:");
puts(" -h | --help\t\tprint this help");
puts(" -x | --hex\t\tprint all characters in hexadecimal form separated by spaces");
puts(" -e | --decompress\tdecompress the input");
puts("by default the input from stdin will be compresssed and written to stdout");
}
int main(int argc, char** argv)
{
options opts = parse_args(argc, argv);
if (opts.print_help)
{
print_help(argv[0]);
}
else if (opts.decompress)
{
decompress(&opts);
}
else
{
compress(&opts);
}
}
As usual any suggestions welcome (portability, standard conformity, best practices, etc.).