Skip to content

Commit ef1f698

Browse files
committed
feat(c-cli): -C/--container — emit & consume .pbf.json (issue #1)
The standalone (portable) C CLI now understands the container: its own vector-pinned pb_crc32 (self-contained, no Zig lib) + its encode_data/decode_data + the shared container_json.h helpers. Transport-resistant, -Wall -Wextra clean. test-container-c guards it (round-trip + self-verify + transport-resistance).
1 parent 7d34d5a commit ef1f698

2 files changed

Lines changed: 89 additions & 0 deletions

File tree

flake.nix

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,19 @@
378378
installPhase = "mkdir -p $out && touch $out/passed";
379379
};
380380

381+
# Container (.pbf.json) for the standalone C CLI (issue #1).
382+
test-container-c = pkgs.stdenv.mkDerivation {
383+
name = "test-container-c";
384+
src = ./.;
385+
nativeBuildInputs = with pkgs; [ clang ];
386+
buildPhase = ''
387+
export HOME=$TMPDIR
388+
clang -O3 -Wall -Wextra -I. -o printable-binary-c src/printable_binary.c
389+
IMPLEMENTATION_TO_TEST=./printable-binary-c bash ./test/test_container
390+
'';
391+
installPhase = "mkdir -p $out && touch $out/passed";
392+
};
393+
381394
# Dogfood the C FFI boundary: build the C FFI CLI against the Zig static
382395
# lib and round-trip through it (encode/decode + hexlike).
383396
test-ffi-cli = pkgs.stdenv.mkDerivation {

src/printable_binary.c

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
#include "character_map_embedded.h"
2828
#include "printable_binary.h"
29+
#include "container_json.h"
2930

3031
#ifdef __EMSCRIPTEN__
3132
// Standalone WASM builds shouldn't depend on host-provided env functions.
@@ -126,6 +127,7 @@ typedef struct {
126127
int64_t range_end;
127128
bool no_double_encode_check;
128129
bool hexlike_mode;
130+
bool container_mode;
129131
} options_t;
130132

131133
static void parse_format_spec(options_t *opts, const char *format_str) {
@@ -801,6 +803,21 @@ pb_double_encode_info_t pb_detect_double_encode(const char *input, size_t input_
801803
return detect_double_encode(input, input_len, threshold);
802804
}
803805

806+
// CRC-32/ISO-HDLC (vector-pinned: CRC32("123456789")=0xCBF43926). Self-contained
807+
// (the standalone build does not link the Zig lib). Backs .pbf.json container
808+
// integrity; bitwise, ample for container-sized payloads.
809+
uint32_t pb_crc32(const char *input, size_t input_len) {
810+
if (!input || input_len == 0) return 0u;
811+
uint32_t crc = 0xFFFFFFFFu;
812+
for (size_t i = 0; i < input_len; i++) {
813+
crc ^= (unsigned char)input[i];
814+
for (int k = 0; k < 8; k++) {
815+
crc = (crc & 1u) ? ((crc >> 1) ^ 0xEDB88320u) : (crc >> 1);
816+
}
817+
}
818+
return ~crc;
819+
}
820+
804821
// Hexlike passthrough set: bytes that pass through as-is in hexlike mode
805822
// . 0-9 @ A-Z ^ _ a-z (the same bytes that are identity-mapped in the character map)
806823
static bool is_hexlike_passthrough(uint8_t byte, bool spaces_mode) {
@@ -1242,6 +1259,8 @@ static void print_usage(const char *program_name) {
12421259
fprintf(stderr, " -p, --passthrough Pass input to stdout unchanged, send encoded data to stderr\n");
12431260
fprintf(stderr, "\nEncoding modes:\n");
12441261
fprintf(stderr, " -X, --hexlike Hexlike mode: passthrough ASCII stays as-is, all other bytes\n");
1262+
fprintf(stderr, " -C, --container Container mode: encode a file to a self-verifying .pbf.json\n");
1263+
fprintf(stderr, " (keeps filename + crc32). With -d, decode a container back.\n");
12451264
fprintf(stderr, " shown as uppercase hex runs prefixed by \xCE\x9F\xCF\x87 (Greek Omicron+Chi,\n");
12461265
fprintf(stderr, " NOT ASCII 0x \xe2\x80\x94 beware when copying hex for other purposes).\n");
12471266
fprintf(stderr, " Use with -d to decode hexlike-encoded data back to binary.\n");
@@ -1533,6 +1552,8 @@ static options_t parse_options(int argc, char *argv[]) {
15331552
opts.no_double_encode_check = true;
15341553
} else if (long_option_equals(name, name_len, "hexlike")) {
15351554
opts.hexlike_mode = true;
1555+
} else if (long_option_equals(name, name_len, "container")) {
1556+
opts.container_mode = true;
15361557
} else if (long_option_equals(name, name_len, "help")) {
15371558
opts.help_mode = true;
15381559
} else {
@@ -1580,6 +1601,10 @@ static options_t parse_options(int argc, char *argv[]) {
15801601
opts.hexlike_mode = true;
15811602
pos++;
15821603
break;
1604+
case 'C':
1605+
opts.container_mode = true;
1606+
pos++;
1607+
break;
15831608
case 'P': {
15841609
if (arg[pos + 1] != '\0') {
15851610
const char *value = &arg[pos + 1];
@@ -1751,6 +1776,57 @@ int main(int argc, char *argv[]) {
17511776
}
17521777
}
17531778

1779+
if (opts.container_mode) {
1780+
if (opts.decode_mode) {
1781+
size_t dlen;
1782+
const char *draw = cj_get_string(input.data, input.size, "data", &dlen);
1783+
if (!draw) { fprintf(stderr, "Error: not a printable-binary-file container (missing 'data')\n"); buffer_free(&input); return 1; }
1784+
size_t clen;
1785+
char *clean = cj_canonical(draw, dlen, &clen);
1786+
size_t celen;
1787+
const char *ce = cj_get_string(input.data, input.size, "crc32_encoded", &celen);
1788+
if (ce) {
1789+
char hx[9]; snprintf(hx, 9, "%08x", (unsigned int)pb_crc32(clean, clen));
1790+
if (celen != 8 || memcmp(hx, ce, 8) != 0) { free(clean); buffer_free(&input); fprintf(stderr, "Error: container crc32_encoded mismatch (data corrupted)\n"); return 1; }
1791+
}
1792+
buffer_t dec = decode_data((uint8_t *)clean, clen, false);
1793+
free(clean);
1794+
size_t colen;
1795+
const char *co = cj_get_string(input.data, input.size, "crc32", &colen);
1796+
if (co) {
1797+
char hx[9]; snprintf(hx, 9, "%08x", (unsigned int)pb_crc32(dec.data, dec.size));
1798+
if (colen != 8 || memcmp(hx, co, 8) != 0) { buffer_free(&dec); buffer_free(&input); fprintf(stderr, "Error: container crc32 mismatch (decoded data corrupted)\n"); return 1; }
1799+
}
1800+
fwrite(dec.data, 1, dec.size, stdout);
1801+
buffer_free(&dec);
1802+
buffer_free(&input);
1803+
return 0;
1804+
} else {
1805+
options_t enc_opts = opts;
1806+
enc_opts.spaces_mode = false;
1807+
enc_opts.tabs_mode = false;
1808+
enc_opts.crlf_mode = false;
1809+
enc_opts.preserve_chars[0] = '\0';
1810+
buffer_t enc = encode_data((uint8_t *)input.data, input.size, &enc_opts);
1811+
size_t clen;
1812+
char *clean = cj_canonical(enc.data, enc.size, &clen);
1813+
char crc_orig[9], crc_enc[9];
1814+
snprintf(crc_orig, 9, "%08x", (unsigned int)pb_crc32(input.data, input.size));
1815+
snprintf(crc_enc, 9, "%08x", (unsigned int)pb_crc32(clean, clen));
1816+
free(clean);
1817+
const char *fname = (opts.input_file && strcmp(opts.input_file, "-") != 0) ? cj_basename(opts.input_file) : "";
1818+
printf("{\n \"format\": \"printable-binary-file\",\n \"version\": 1,\n \"filename\": \"");
1819+
cj_fputs_escaped(stdout, fname, strlen(fname));
1820+
printf("\",\n \"byte_length\": %zu,\n \"crc32\": \"%s\",\n \"crc32_encoded\": \"%s\",\n \"data\": \"", input.size, crc_orig, crc_enc);
1821+
fwrite(enc.data, 1, enc.size, stdout);
1822+
printf("\"\n}\n");
1823+
buffer_free(&enc);
1824+
buffer_free(&input);
1825+
return 0;
1826+
}
1827+
}
1828+
1829+
17541830
if (opts.decode_mode) {
17551831
if (opts.passthrough_mode) {
17561832
fprintf(stderr, "Warning: --passthrough ignored in decode mode\n");

0 commit comments

Comments
 (0)