Skip to content

Commit 4d8f461

Browse files
committed
feat(ffi): expose hexlike through the C ABI (no more bypass-only feature)
Hexlike (-X) lived only in the Zig core and was reachable solely by importing the core directly; the FFI CLI rejected -X entirely. Add pb_hexlike_encode, pb_hexlike_decode, and pb_detect_hexlike exports (null-guarded, FFIResult-based, matching the existing pb_* pattern) to the Zig core + printable_binary.h, and wire -X/--hexlike into the FFI CLI so it routes through those exports. Verified: Zig unit test (FFI hexlike roundtrip + detect + null safety) and an end-to-end FFI-CLI build — 'Hi\xff! World' -> 'Hi ΟχFF2120 World' -> roundtrips.
1 parent 8d899bf commit 4d8f461

3 files changed

Lines changed: 86 additions & 3 deletions

File tree

src/printable_binary.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,34 @@ typedef struct {
244244
*/
245245
pb_double_encode_info_t pb_detect_double_encode(const char *input, size_t input_len, float threshold);
246246

247+
/* ============================================================================
248+
* Hexlike API (passthrough ASCII stays as-is; other bytes -> Οχ-prefixed hex)
249+
* ============================================================================ */
250+
251+
/**
252+
* Encode binary data to hexlike format. Caller must call pb_free() on result.data.
253+
* @param input Binary data (may be NULL only when input_len == 0)
254+
* @param input_len Length of input in bytes
255+
* @param spaces Non-zero to preserve literal spaces as passthrough
256+
* @return Result with hexlike-encoded data or error
257+
*/
258+
pb_ffi_result_t pb_hexlike_encode(const char *input, size_t input_len, int spaces);
259+
260+
/**
261+
* Decode hexlike text back to binary. Caller must call pb_free() on result.data.
262+
* @param input Hexlike-encoded text (may be NULL only when input_len == 0)
263+
* @param input_len Length of input in bytes
264+
* @param spaces Non-zero to treat literal spaces as data
265+
* @return Result with decoded data or error
266+
*/
267+
pb_ffi_result_t pb_hexlike_decode(const char *input, size_t input_len, int spaces);
268+
269+
/**
270+
* Detect whether input appears to be hexlike-encoded.
271+
* @return 1 if hexlike, 0 otherwise (also 0 on NULL with input_len > 0)
272+
*/
273+
int pb_detect_hexlike(const char *input, size_t input_len);
274+
247275
/* ============================================================================
248276
* C Implementation Only
249277
* ============================================================================ */

src/printable_binary_ffi_main.c

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ typedef struct {
2929
bool tabs_mode;
3030
bool crlf_mode;
3131
bool strip_whitespace;
32+
bool hexlike_mode;
3233
bool format_mode;
3334
bool help_mode;
3435
int format_group;
@@ -61,6 +62,7 @@ static void print_usage(const char *name) {
6162
fprintf(stderr, "Options:\n");
6263
fprintf(stderr, " -d, --decode Decode mode (default is encode mode)\n");
6364
fprintf(stderr, " -p, --passthrough Pass input to stdout unchanged, send encoded data to stderr\n");
65+
fprintf(stderr, " -X, --hexlike Hexlike mode (passthrough ASCII as-is, other bytes as \xce\x9f\xcf\x87-prefixed hex)\n");
6466
fprintf(stderr, "\nEncode options (preserve literal characters instead of encoding):\n");
6567
fprintf(stderr, " -s, --spaces Preserve literal spaces\n");
6668
fprintf(stderr, " -t, --tabs Preserve literal tabs\n");
@@ -230,6 +232,8 @@ static options_t parse_options(int argc, char *argv[]) {
230232
opts.decode_mode = true;
231233
} else if (strcmp(name, "passthrough") == 0) {
232234
opts.passthrough_mode = true;
235+
} else if (strcmp(name, "hexlike") == 0) {
236+
opts.hexlike_mode = true;
233237
} else if (strcmp(name, "spaces") == 0) {
234238
opts.spaces_mode = true;
235239
} else if (strcmp(name, "tabs") == 0) {
@@ -319,6 +323,7 @@ static options_t parse_options(int argc, char *argv[]) {
319323
switch (arg[j]) {
320324
case 'd': opts.decode_mode = true; break;
321325
case 'p': opts.passthrough_mode = true; break;
326+
case 'X': opts.hexlike_mode = true; break;
322327
case 's': opts.spaces_mode = true; break;
323328
case 't': opts.tabs_mode = true; break;
324329
case 'n': opts.crlf_mode = true; break;
@@ -540,7 +545,9 @@ int main(int argc, char *argv[]) {
540545
if (opts.spaces_mode) flags |= PB_DECODE_SPACES_MODE;
541546
if (opts.strip_whitespace) flags |= PB_DECODE_STRIP_WS;
542547

543-
pb_ffi_result_t result = pb_decode(input, input_len, flags);
548+
pb_ffi_result_t result = opts.hexlike_mode
549+
? pb_hexlike_decode(input, input_len, opts.spaces_mode ? 1 : 0)
550+
: pb_decode(input, input_len, flags);
544551
if (result.error_code != 0 || !result.data) {
545552
fprintf(stderr, "Decode error\n");
546553
free(input);
@@ -578,8 +585,9 @@ int main(int argc, char *argv[]) {
578585
if (opts.crlf_mode) flags |= PB_ENCODE_PRESERVE_CRLF;
579586

580587
size_t preserve_len = opts.preserve_chars ? strlen(opts.preserve_chars) : 0;
581-
pb_ffi_result_t result = pb_encode(input, input_len, flags,
582-
opts.preserve_chars, preserve_len);
588+
pb_ffi_result_t result = opts.hexlike_mode
589+
? pb_hexlike_encode(input, input_len, opts.spaces_mode ? 1 : 0)
590+
: pb_encode(input, input_len, flags, opts.preserve_chars, preserve_len);
583591
if (result.error_code != 0 || !result.data) {
584592
fprintf(stderr, "Encode error\n");
585593
free(input);

src/zig/printable_binary.zig

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,37 @@ export fn pb_detect_double_encode(input: ?[*]const u8, input_len: usize, thresho
357357
return detectDoubleEncode(slice, threshold);
358358
}
359359

360+
/// C ABI export for hexlike encoding. Caller must call pb_free() on result.data.
361+
export fn pb_hexlike_encode(input: ?[*]const u8, input_len: usize, spaces: c_int) callconv(.c) FFIResult {
362+
if (input == null and input_len > 0) {
363+
return FFIResult{ .data = null, .len = 0, .error_code = 1 };
364+
}
365+
const input_slice = if (input_len > 0) input.?[0..input_len] else &[_]u8{};
366+
const result = hexlikeEncode(ffi_allocator, input_slice, .{ .spaces = spaces != 0 }) catch {
367+
return FFIResult{ .data = null, .len = 0, .error_code = 1 };
368+
};
369+
return FFIResult{ .data = result.ptr, .len = result.len, .error_code = 0 };
370+
}
371+
372+
/// C ABI export for hexlike decoding. Caller must call pb_free() on result.data.
373+
export fn pb_hexlike_decode(input: ?[*]const u8, input_len: usize, spaces: c_int) callconv(.c) FFIResult {
374+
if (input == null and input_len > 0) {
375+
return FFIResult{ .data = null, .len = 0, .error_code = 1 };
376+
}
377+
const input_slice = if (input_len > 0) input.?[0..input_len] else &[_]u8{};
378+
const result = hexlikeDecode(ffi_allocator, input_slice, .{ .spaces = spaces != 0 }) catch {
379+
return FFIResult{ .data = null, .len = 0, .error_code = 1 };
380+
};
381+
return FFIResult{ .data = result.data.ptr, .len = result.data.len, .error_code = 0 };
382+
}
383+
384+
/// C ABI export for hexlike detection. Returns 1 if input appears hexlike, else 0.
385+
export fn pb_detect_hexlike(input: ?[*]const u8, input_len: usize) callconv(.c) c_int {
386+
if (input == null and input_len > 0) return 0;
387+
const input_slice = if (input_len > 0) input.?[0..input_len] else &[_]u8{};
388+
return if (detectHexlike(input_slice)) 1 else 0;
389+
}
390+
360391
/// Get UTF-8 sequence length from first byte
361392
pub fn utf8SeqLen(first_byte: u8) u3 {
362393
if (first_byte < 0x80) return 1;
@@ -1478,3 +1509,19 @@ test "parseGlyphLine: filters comments/blank, strips trailing comment, takes fir
14781509
// CRLF tolerance
14791510
try std.testing.expectEqualStrings("·", parseGlyphLine(\r").?);
14801511
}
1512+
1513+
test "FFI: pb_hexlike_encode/decode roundtrip + detect + null safety" {
1514+
const input = "Hi\xff!";
1515+
const enc = pb_hexlike_encode(input.ptr, input.len, 0);
1516+
try std.testing.expect(enc.error_code == 0);
1517+
defer pb_free(enc.data, enc.len);
1518+
try std.testing.expect(pb_detect_hexlike(enc.data, enc.len) == 1);
1519+
const dec = pb_hexlike_decode(enc.data, enc.len, 0);
1520+
try std.testing.expect(dec.error_code == 0);
1521+
defer pb_free(dec.data, dec.len);
1522+
try std.testing.expectEqualStrings(input, dec.data.?[0..dec.len]);
1523+
// null safety
1524+
const bad = pb_hexlike_encode(null, 100, 0);
1525+
try std.testing.expect(bad.error_code != 0);
1526+
try std.testing.expect(pb_detect_hexlike(null, 100) == 0);
1527+
}

0 commit comments

Comments
 (0)