From d31c2941c929e9bcda97f9206b121e8ebdefac76 Mon Sep 17 00:00:00 2001 From: Manu Garg Date: Fri, 17 Apr 2026 00:25:50 -0700 Subject: [PATCH 1/3] Add alert() and console.log() for PAC script debugging Exposes alert(msg) and console.log(...args) in the JavaScript context so PAC authors can log from inside large PAC files. Output is routed through the existing error printer (stderr by default, overridable via pacparser_set_error_printer), so pactester's stdout proxy result stays clean. Addresses #32. Co-Authored-By: Claude Opus 4.7 --- src/pacparser.c | 41 +++++++++++++++++++++++++++++++++++++++++ tests/logging.pac | 7 +++++++ tests/runtests.sh | 24 ++++++++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 tests/logging.pac diff --git a/src/pacparser.c b/src/pacparser.c index f5fed8a..9b2ca39 100644 --- a/src/pacparser.c +++ b/src/pacparser.c @@ -217,6 +217,38 @@ dns_resolve_ex(JSContext *ctx, JSValueConst UNUSED(this_val), int UNUSED(argc), return JS_NewString(ctx, ipaddr); } +// Shared helper for alert() and console.log(): concatenates all arguments with +// spaces and prints them via the error printer (stderr by default). Intended +// purely for debugging PAC scripts; production PAC files are not expected to +// call these. +static JSValue +js_log_print(JSContext *ctx, int argc, JSValueConst *argv, const char *prefix) +{ + print_error("%s:", prefix); + for (int i = 0; i < argc; i++) { + const char *s = JS_ToCString(ctx, argv[i]); + if (!s) return JS_EXCEPTION; + print_error(" %s", s); + JS_FreeCString(ctx, s); + } + print_error("\n"); + return JS_UNDEFINED; +} + +// alert(msg) in JS context; routes message to the error printer. +static JSValue +pac_alert(JSContext *ctx, JSValueConst UNUSED(this_val), int argc, JSValueConst *argv) +{ + return js_log_print(ctx, argc, argv, "ALERT"); +} + +// console.log(...args) in JS context; routes message to the error printer. +static JSValue +pac_console_log(JSContext *ctx, JSValueConst UNUSED(this_val), int argc, JSValueConst *argv) +{ + return js_log_print(ctx, argc, argv, "LOG"); +} + // myIpAddress in JS context; not available in core JavaScript. // returns 127.0.0.1 if not able to determine local ip. static JSValue @@ -318,6 +350,15 @@ pacparser_init() JS_SetPropertyStr(ctx, global, "myIpAddressEx", JS_NewCFunction(ctx, my_ip_ex, "myIpAddressEx", 0)); + // Debug logging helpers for PAC scripts. Output goes through the error + // printer (stderr by default). + JS_SetPropertyStr(ctx, global, "alert", + JS_NewCFunction(ctx, pac_alert, "alert", 1)); + JSValue console = JS_NewObject(ctx); + JS_SetPropertyStr(ctx, console, "log", + JS_NewCFunction(ctx, pac_console_log, "log", 1)); + JS_SetPropertyStr(ctx, global, "console", console); + // Evaluate pacUtils. Utility functions required to parse pac files. JSValue result = JS_Eval(ctx, pacUtils, strlen(pacUtils), "pac_utils", JS_EVAL_TYPE_GLOBAL); diff --git a/tests/logging.pac b/tests/logging.pac new file mode 100644 index 0000000..ec9b8cd --- /dev/null +++ b/tests/logging.pac @@ -0,0 +1,7 @@ +// Test PAC file exercising alert() and console.log() debug helpers. +function FindProxyForURL(url, host) { + alert("checking", host); + console.log("url:", url); + console.log("single arg"); + return "DIRECT"; +} diff --git a/tests/runtests.sh b/tests/runtests.sh index 5361aea..63e8aff 100755 --- a/tests/runtests.sh +++ b/tests/runtests.sh @@ -55,4 +55,28 @@ while read line fi done < $testdata + +# Logging tests: verify alert() / console.log() output lands on stderr with +# the expected prefixes, and that the proxy result on stdout is unaffected. +logging_pac=$script_dir/logging.pac +logging_stderr=$(mktemp) +logging_stdout=$($pactester -p $logging_pac -u http://example.com/ -h example.com 2>$logging_stderr) +if [ "$logging_stdout" != "DIRECT" ]; then + echo "Logging test failed: stdout was \"$logging_stdout\", expected \"DIRECT\"" + cat $logging_stderr + rm -f $logging_stderr + exit 1 +fi +expected_stderr=$'ALERT: checking example.com\nLOG: url: http://example.com/\nLOG: single arg' +actual_stderr=$(cat $logging_stderr) +rm -f $logging_stderr +if [ "$actual_stderr" != "$expected_stderr" ]; then + echo "Logging test failed: stderr mismatch" + echo "Expected:" + echo "$expected_stderr" + echo "Got:" + echo "$actual_stderr" + exit 1 +fi + echo "All tests were successful." From 285c37c6374d942e07656f082e07a8572eeaee29 Mon Sep 17 00:00:00 2001 From: Manu Garg Date: Fri, 17 Apr 2026 00:37:49 -0700 Subject: [PATCH 2/3] Trim verbose comments and fix partial log line on stringify failure Match the terse comment style of surrounding helpers (dns_resolve, my_ip). When JS_ToCString fails mid-argument, emit a trailing newline before returning so stderr does not carry a dangling partial line. Co-Authored-By: Claude Opus 4.7 --- src/pacparser.c | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/pacparser.c b/src/pacparser.c index 9b2ca39..7293ddc 100644 --- a/src/pacparser.c +++ b/src/pacparser.c @@ -217,17 +217,17 @@ dns_resolve_ex(JSContext *ctx, JSValueConst UNUSED(this_val), int UNUSED(argc), return JS_NewString(ctx, ipaddr); } -// Shared helper for alert() and console.log(): concatenates all arguments with -// spaces and prints them via the error printer (stderr by default). Intended -// purely for debugging PAC scripts; production PAC files are not expected to -// call these. +// Prints space-separated args via the error printer (stderr by default). static JSValue js_log_print(JSContext *ctx, int argc, JSValueConst *argv, const char *prefix) { print_error("%s:", prefix); for (int i = 0; i < argc; i++) { const char *s = JS_ToCString(ctx, argv[i]); - if (!s) return JS_EXCEPTION; + if (!s) { + print_error("\n"); + return JS_EXCEPTION; + } print_error(" %s", s); JS_FreeCString(ctx, s); } @@ -235,14 +235,12 @@ js_log_print(JSContext *ctx, int argc, JSValueConst *argv, const char *prefix) return JS_UNDEFINED; } -// alert(msg) in JS context; routes message to the error printer. static JSValue pac_alert(JSContext *ctx, JSValueConst UNUSED(this_val), int argc, JSValueConst *argv) { return js_log_print(ctx, argc, argv, "ALERT"); } -// console.log(...args) in JS context; routes message to the error printer. static JSValue pac_console_log(JSContext *ctx, JSValueConst UNUSED(this_val), int argc, JSValueConst *argv) { @@ -350,8 +348,6 @@ pacparser_init() JS_SetPropertyStr(ctx, global, "myIpAddressEx", JS_NewCFunction(ctx, my_ip_ex, "myIpAddressEx", 0)); - // Debug logging helpers for PAC scripts. Output goes through the error - // printer (stderr by default). JS_SetPropertyStr(ctx, global, "alert", JS_NewCFunction(ctx, pac_alert, "alert", 1)); JSValue console = JS_NewObject(ctx); From 948c74732ed031ffc74c367caef463437470b2a1 Mon Sep 17 00:00:00 2001 From: Manu Garg Date: Fri, 17 Apr 2026 00:40:55 -0700 Subject: [PATCH 3/3] tests: add Python-side logging test Extend runtests.py with a check that alert()/console.log() from a PAC file emit the expected ALERT:/LOG: lines on stderr. Since the _pacparser C extension writes via vfprintf on fd 2, stderr is captured at the file-descriptor level with dup2 rather than by reassigning sys.stderr. Co-Authored-By: Claude Opus 4.7 --- tests/runtests.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/runtests.py b/tests/runtests.py index ba48699..4c55191 100644 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -24,6 +24,7 @@ import glob import os import sys +import tempfile def module_path(tests_dir): py_ver = '*'.join([str(x) for x in sys.version_info[0:2]]) @@ -75,6 +76,34 @@ def runtests(pacfile, testdata, tests_dir): pacparser.cleanup() if result != expected_result: raise Exception('Tests failed. Got "%s", expected "%s"' % (result, expected_result)) + + # Logging test: alert() / console.log() should emit to the C library's + # stderr. The extension writes directly via vfprintf, so redirect at the + # file-descriptor level rather than via sys.stderr. + logging_pac = os.path.join(tests_dir, 'logging.pac') + expected_stderr = ('ALERT: checking example.com\n' + 'LOG: url: http://example.com/\n' + 'LOG: single arg\n') + with tempfile.TemporaryFile(mode='w+') as tmp: + saved_fd = os.dup(2) + os.dup2(tmp.fileno(), 2) + try: + pacparser.init() + pacparser.parse_pac_file(logging_pac) + proxy = pacparser.find_proxy('http://example.com/', 'example.com') + pacparser.cleanup() + finally: + sys.stderr.flush() + os.dup2(saved_fd, 2) + os.close(saved_fd) + tmp.seek(0) + actual_stderr = tmp.read() + if proxy != 'DIRECT': + raise Exception('Logging test failed: proxy was "%s", expected "DIRECT"' % proxy) + if actual_stderr != expected_stderr: + raise Exception('Logging test failed: stderr mismatch\nExpected:\n%s\nGot:\n%s' % + (expected_stderr, actual_stderr)) + print('All tests were successful.')