diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b3a6278..c1fe378f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -393,6 +393,8 @@ jobs: - run: zig build - name: Run ./tests/extensions/run (dynamic and static) run: STATIC=1 ./tests/extensions/run + - name: Run ./tests/ini_test + run: ./tests/ini_test compile: needs: changes diff --git a/Makefile b/Makefile index 2757e7fb..849d3516 100644 --- a/Makefile +++ b/Makefile @@ -44,6 +44,10 @@ fuzz: build ## Mutation-fuzz the pipeline and decoders on the Debug build (FUZZ_ ext: build ## Run extension API tests (builds tests/extensions/demo.c with zig cc; STATIC=1 also builds a zphp with it compiled in, BENCH=1 prints call overhead) ./tests/extensions/run +.PHONY: ini +ini: build ## Run php.ini loading tests (--ini, ZPHP_INI, -d, serve isolation, extension= lines) + ./tests/ini_test + .PHONY: soak soak: ## Run the memory soak (ReleaseFast): a string-heavy loop must hold a flat RSS zig build -Doptimize=ReleaseFast diff --git a/README.md b/README.md index 0128c76b..346d9fca 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,17 @@ The glibc and macOS builds run on the platform they were made for; the musl buil See the [documentation](https://nvms.github.io/zphp/) for build instructions and usage guides. +## Configuration + +zphp reads a php.ini file at startup: `--ini=PATH`, then the `ZPHP_INI` environment variable, then `php.ini` in the working directory, whichever comes first. `-d name=value` overrides a directive from the command line, as with `php -d`. + +```sh +zphp --ini=/etc/zphp/php.ini serve public/index.php +zphp -d date.timezone=Europe/Berlin -d memory_limit=512M run job.php +``` + +Values follow php.ini rules: quotes are stripped, `On`/`Off` become `1` and empty, `${VAR}` expands from the environment, and `error_reporting = E_ALL & ~E_DEPRECATED` is evaluated. `date.timezone`, `error_reporting`, and `max_execution_time` take effect at the start of every request, and `ini_set` only changes the current request; the next one starts from the file again. `extension=` lines load extensions, by path or by name inside `extension_dir`, and `php_ini_loaded_file()` reports what was read. + ## Extensions zphp loads native extensions written against a C ABI. An extension registers functions, classes, constants, ini defaults, and resource types once at load time, and every PHP call into it goes straight to a resolved function pointer. The header is `include/zphp_extension.h`; the same source builds as a shared library or gets compiled into zphp. diff --git a/src/ini_config.zig b/src/ini_config.zig new file mode 100644 index 00000000..a3c765c9 --- /dev/null +++ b/src/ini_config.zig @@ -0,0 +1,266 @@ +// php.ini support: one file read at startup into a process-lifetime list of +// directives, applied to every VM at the start of each request beneath the +// request-scoped ini_set layer. `extension=` lines load extensions through +// the same loader as --extension +const std = @import("std"); +const vm_mod = @import("runtime/vm.zig"); +const VM = vm_mod.VM; +const RuntimeError = vm_mod.RuntimeError; +const extension = @import("extension.zig"); + +pub const Directive = struct { name: []const u8, value: []const u8 }; + +var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); +var directives: std.ArrayListUnmanaged(Directive) = .{}; +var loaded_file: ?[]const u8 = null; +var extension_dir: ?[]const u8 = null; + +fn persistent() std.mem.Allocator { + return arena.allocator(); +} + +fn fail(comptime fmt: []const u8, args: anytype) noreturn { + std.debug.print("zphp: " ++ fmt ++ "\n", args); + std.process.exit(1); +} + +pub fn loadedFile() ?[]const u8 { + return loaded_file; +} + +pub fn all() []const Directive { + return directives.items; +} + +pub fn get(name: []const u8) ?[]const u8 { + var found: ?[]const u8 = null; + for (directives.items) |d| if (std.mem.eql(u8, d.name, name)) { + found = d.value; + }; + return found; +} + +// `zphp --ini=PATH` names the file; otherwise ZPHP_INI, then php.ini in the +// working directory, the way php-cli looks next to where it runs. an +// explicit path must exist; the discovered ones are optional +pub fn discover(explicit: ?[]const u8) void { + if (explicit) |path| return load(path, true); + if (std.posix.getenv("ZPHP_INI")) |path| return load(path, true); + load("php.ini", false); +} + +pub fn load(path: []const u8, required: bool) void { + const bytes = std.fs.cwd().readFileAlloc(persistent(), path, 16 << 20) catch |err| { + if (required) fail("ini file '{s}': {s}", .{ path, @errorName(err) }); + return; + }; + loaded_file = std.fs.cwd().realpathAlloc(persistent(), path) catch persistent().dupe(u8, path) catch fail("out of memory", .{}); + parse(bytes, path); +} + +// `-d name=value` on the command line, applied after the file +pub fn define(assignment: []const u8) void { + const eq = std.mem.indexOfScalar(u8, assignment, '=') orelse { + add(std.mem.trim(u8, assignment, " \t"), "1"); + return; + }; + add(std.mem.trim(u8, assignment[0..eq], " \t"), cook(std.mem.trim(u8, assignment[eq + 1 ..], " \t"))); +} + +fn add(name: []const u8, value: []const u8) void { + if (name.len == 0) return; + if (std.mem.eql(u8, name, "extension")) { + loadExtensionDirective(value); + return; + } + if (std.mem.eql(u8, name, "zend_extension")) return; + if (std.mem.eql(u8, name, "extension_dir")) extension_dir = value; + directives.append(persistent(), .{ .name = name, .value = value }) catch fail("out of memory", .{}); +} + +fn parse(bytes: []const u8, path: []const u8) void { + var lines = std.mem.splitScalar(u8, bytes, '\n'); + var line_no: usize = 0; + while (lines.next()) |raw| { + line_no += 1; + var line = std.mem.trim(u8, raw, " \t\r"); + if (line.len == 0 or line[0] == ';' or line[0] == '#' or line[0] == '[') continue; + line = stripComment(line); + const eq = std.mem.indexOfScalar(u8, line, '=') orelse fail("ini file '{s}' line {d}: expected name = value", .{ path, line_no }); + const name = std.mem.trim(u8, line[0..eq], " \t"); + const value = std.mem.trim(u8, line[eq + 1 ..], " \t"); + add(persistent().dupe(u8, name) catch fail("out of memory", .{}), cook(value)); + } +} + +// a ; outside quotes starts a comment +fn stripComment(line: []const u8) []const u8 { + var quoted = false; + for (line, 0..) |ch, i| { + if (ch == '"') quoted = !quoted; + if (ch == ';' and !quoted) return std.mem.trim(u8, line[0..i], " \t"); + } + return line; +} + +// php's ini value rules: quotes are stripped, On/Yes/True become "1" and +// Off/No/None/False become "", ${VAR} expands from the environment, and an +// expression over E_* constants (E_ALL & ~E_DEPRECATED) is evaluated +fn cook(raw: []const u8) []const u8 { + if (raw.len >= 2 and raw[0] == '"' and raw[raw.len - 1] == '"') return expandEnv(raw[1 .. raw.len - 1]); + if (raw.len == 0) return ""; + inline for (.{ "on", "yes", "true" }) |word| if (std.ascii.eqlIgnoreCase(raw, word)) return "1"; + inline for (.{ "off", "no", "none", "false", "null" }) |word| if (std.ascii.eqlIgnoreCase(raw, word)) return ""; + if (errorLevelExpression(raw)) |level| return std.fmt.allocPrint(persistent(), "{d}", .{level}) catch fail("out of memory", .{}); + return expandEnv(raw); +} + +fn expandEnv(value: []const u8) []const u8 { + if (std.mem.indexOf(u8, value, "${") == null) return persistent().dupe(u8, value) catch fail("out of memory", .{}); + var out = std.ArrayListUnmanaged(u8){}; + var i: usize = 0; + while (i < value.len) { + if (value[i] == '$' and i + 1 < value.len and value[i + 1] == '{') { + if (std.mem.indexOfScalarPos(u8, value, i + 2, '}')) |close| { + const var_name = value[i + 2 .. close]; + const name_z = persistent().dupeZ(u8, var_name) catch fail("out of memory", .{}); + if (std.posix.getenv(name_z)) |env| out.appendSlice(persistent(), env) catch fail("out of memory", .{}); + i = close + 1; + continue; + } + } + out.append(persistent(), value[i]) catch fail("out of memory", .{}); + i += 1; + } + return out.items; +} + +const error_levels = [_]struct { name: []const u8, value: i64 }{ + .{ .name = "E_ERROR", .value = 1 }, .{ .name = "E_WARNING", .value = 2 }, .{ .name = "E_PARSE", .value = 4 }, + .{ .name = "E_NOTICE", .value = 8 }, .{ .name = "E_CORE_ERROR", .value = 16 }, .{ .name = "E_CORE_WARNING", .value = 32 }, + .{ .name = "E_COMPILE_ERROR", .value = 64 }, .{ .name = "E_COMPILE_WARNING", .value = 128 }, .{ .name = "E_USER_ERROR", .value = 256 }, + .{ .name = "E_USER_WARNING", .value = 512 }, .{ .name = "E_USER_NOTICE", .value = 1024 }, .{ .name = "E_STRICT", .value = 2048 }, + .{ .name = "E_RECOVERABLE_ERROR", .value = 4096 }, .{ .name = "E_DEPRECATED", .value = 8192 }, .{ .name = "E_USER_DEPRECATED", .value = 16384 }, + // php 8.4 dropped E_STRICT from E_ALL; this is also zphp's default level + .{ .name = "E_ALL", .value = 30719 }, +}; + +// grammar: expr = term (('|' | '&' | '^') term)*; term = '~' term | '(' expr ')' | E_NAME | integer +const LevelParser = struct { + src: []const u8, + pos: usize = 0, + ok: bool = true, + + fn skip(self: *LevelParser) void { + while (self.pos < self.src.len and (self.src[self.pos] == ' ' or self.src[self.pos] == '\t')) self.pos += 1; + } + + fn expr(self: *LevelParser) i64 { + var acc = self.term(); + while (true) { + self.skip(); + if (self.pos >= self.src.len) return acc; + const op = self.src[self.pos]; + if (op == ')') return acc; + if (op != '|' and op != '&' and op != '^') { + self.ok = false; + return acc; + } + self.pos += 1; + const rhs = self.term(); + acc = switch (op) { + '|' => acc | rhs, + '&' => acc & rhs, + else => acc ^ rhs, + }; + } + } + + fn term(self: *LevelParser) i64 { + self.skip(); + if (self.pos >= self.src.len) { + self.ok = false; + return 0; + } + const ch = self.src[self.pos]; + if (ch == '~') { + self.pos += 1; + return ~self.term(); + } + if (ch == '(') { + self.pos += 1; + const inner = self.expr(); + self.skip(); + if (self.pos < self.src.len and self.src[self.pos] == ')') self.pos += 1 else self.ok = false; + return inner; + } + const start = self.pos; + while (self.pos < self.src.len and (std.ascii.isAlphanumeric(self.src[self.pos]) or self.src[self.pos] == '_')) self.pos += 1; + const word = self.src[start..self.pos]; + if (word.len == 0) { + self.ok = false; + return 0; + } + if (std.fmt.parseInt(i64, word, 10)) |n| return n else |_| {} + for (error_levels) |level| if (std.mem.eql(u8, level.name, word)) return level.value; + self.ok = false; + return 0; + } +}; + +fn errorLevelExpression(raw: []const u8) ?i64 { + if (std.mem.indexOf(u8, raw, "E_") == null) return null; + var p = LevelParser{ .src = raw }; + const v = p.expr(); + if (!p.ok or p.pos != raw.len) return null; + return v; +} + +// extension=name resolves like php: a bare name lives in extension_dir (or +// next to the ini file) and gets the platform suffix +fn loadExtensionDirective(value: []const u8) void { + if (value.len == 0) return; + const has_dir = std.mem.indexOfScalar(u8, value, '/') != null; + const suffix: []const u8 = switch (@import("builtin").os.tag) { + .macos, .ios => ".dylib", + .windows => ".dll", + else => ".so", + }; + const with_suffix = if (std.mem.indexOfScalar(u8, value, '.') == null) std.mem.concat(persistent(), u8, &.{ value, suffix }) catch fail("out of memory", .{}) else value; + if (has_dir) return extension.loadDynamic(with_suffix); + const dir = extension_dir orelse if (loaded_file) |f| std.fs.path.dirname(f) orelse "." else "."; + const full = std.fs.path.join(persistent(), &.{ dir, with_suffix }) catch fail("out of memory", .{}); + extension.loadDynamic(full); +} + +// seed the request layer and mirror the directives the VM keeps as state; +// runs at the top of every request, after reset cleared the previous one +pub fn applyToVm(vm: *VM) RuntimeError!void { + for (directives.items) |d| { + try vm.ini_settings.put(vm.allocator, d.name, d.value); + if (std.mem.eql(u8, d.name, "error_reporting")) { + vm.error_reporting_level = std.fmt.parseInt(i64, std.mem.trim(u8, d.value, " \t"), 10) catch vm.error_reporting_level; + } else if (std.mem.eql(u8, d.name, "date.timezone")) { + if (d.value.len > 0) vm.default_tz_name = d.value; + } else if (std.mem.eql(u8, d.name, "max_execution_time")) { + vm.setExecutionLimit(std.fmt.parseInt(i64, std.mem.trim(u8, d.value, " \t"), 10) catch 0); + } + } +} + +test "ini values are cooked the way php cooks them" { + try std.testing.expectEqualStrings("1", cook("On")); + try std.testing.expectEqualStrings("", cook("Off")); + try std.testing.expectEqualStrings("America/New_York", cook("\"America/New_York\"")); + try std.testing.expectEqualStrings("22527", cook("E_ALL & ~E_DEPRECATED")); + try std.testing.expectEqualStrings("22519", cook("E_ALL & ~(E_NOTICE | E_DEPRECATED)")); + try std.testing.expectEqualStrings("E_ALL &", cook("E_ALL &")); + try std.testing.expectEqualStrings("E_ALL)", cook("E_ALL)")); + try std.testing.expectEqualStrings("(E_ALL", cook("(E_ALL")); + try std.testing.expectEqualStrings("256M", cook("256M")); +} + +test "a semicolon outside quotes starts a comment" { + try std.testing.expectEqualStrings("a = b", stripComment("a = b ; note")); + try std.testing.expectEqualStrings("a = \"x;y\"", stripComment("a = \"x;y\"")); +} diff --git a/src/main.zig b/src/main.zig index 60122439..d3af72d9 100644 --- a/src/main.zig +++ b/src/main.zig @@ -6,6 +6,7 @@ const VM = @import("runtime/vm.zig").VM; const Value = runtime_value.Value; const CompileResult = @import("pipeline/compiler.zig").CompileResult; const extension = @import("extension.zig"); +const ini_config = @import("ini_config.zig"); const bytecode_format = @import("bytecode_format.zig"); const error_format = @import("error_format.zig"); @@ -35,7 +36,7 @@ pub fn main() !void { return; } - const args = try loadExtensionFlags(allocator, raw_args); + const args = try loadStartupFlags(allocator, raw_args); defer allocator.free(args); if (args.len < 2) { @@ -46,12 +47,18 @@ pub fn main() !void { try dispatch(allocator, args); } -// `zphp [--extension=PATH]... ...` loads dynamic extensions before -// any VM exists, then ZPHP_EXTENSION_DIR adds every library in that -// directory. returns the args with the flags removed -fn loadExtensionFlags(allocator: std.mem.Allocator, raw_args: []const []const u8) ![]const []const u8 { +// `zphp [--extension=PATH]... [--ini=PATH] [-d name=value]... ...` +// loads dynamic extensions and the ini file before any VM exists. the ini +// file comes from --ini, ZPHP_INI, or php.ini in the working directory; its +// extension= lines load after the flags, -d definitions apply last, then +// ZPHP_EXTENSION_DIR adds every library in that directory. returns the args +// with the flags removed +fn loadStartupFlags(allocator: std.mem.Allocator, raw_args: []const []const u8) ![]const []const u8 { var args = std.ArrayListUnmanaged([]const u8){}; errdefer args.deinit(allocator); + var defines = std.ArrayListUnmanaged([]const u8){}; + defer defines.deinit(allocator); + var ini_path: ?[]const u8 = null; try args.append(allocator, raw_args[0]); var i: usize = 1; while (i < raw_args.len) : (i += 1) { @@ -59,21 +66,37 @@ fn loadExtensionFlags(allocator: std.mem.Allocator, raw_args: []const []const u8 if (std.mem.startsWith(u8, arg, "--extension=")) { extension.loadDynamic(arg["--extension=".len..]); } else if (std.mem.eql(u8, arg, "--extension")) { - if (i + 1 >= raw_args.len) { - try writeStderr("usage: zphp --extension=PATH \n"); - std.process.exit(1); - } i += 1; - extension.loadDynamic(raw_args[i]); + extension.loadDynamic(try flagValue(raw_args, i, "usage: zphp --extension=PATH \n")); + } else if (std.mem.startsWith(u8, arg, "--ini=")) { + ini_path = arg["--ini=".len..]; + } else if (std.mem.eql(u8, arg, "--ini")) { + i += 1; + ini_path = try flagValue(raw_args, i, "usage: zphp --ini=PATH \n"); + } else if (std.mem.startsWith(u8, arg, "-d") and arg.len > 2) { + try defines.append(allocator, arg[2..]); + } else if (std.mem.eql(u8, arg, "-d") or std.mem.eql(u8, arg, "--define")) { + i += 1; + try defines.append(allocator, try flagValue(raw_args, i, "usage: zphp -d name=value \n")); } else { try args.appendSlice(allocator, raw_args[i..]); break; } } + ini_config.discover(ini_path); + for (defines.items) |d| ini_config.define(d); if (std.posix.getenv("ZPHP_EXTENSION_DIR")) |dir| extension.loadDirectory(dir); return args.toOwnedSlice(allocator); } +fn flagValue(raw_args: []const []const u8, i: usize, usage: []const u8) ![]const u8 { + if (i >= raw_args.len) { + try writeStderr(usage); + std.process.exit(1); + } + return raw_args[i]; +} + fn dispatch(allocator: std.mem.Allocator, args: []const []const u8) !void { const cmd = args[1]; @@ -695,6 +718,7 @@ fn writeStderr(msg: []const u8) !void { } test { + _ = @import("ini_config.zig"); _ = @import("pipeline/token.zig"); _ = @import("pipeline/lexer.zig"); _ = @import("pipeline/ast.zig"); diff --git a/src/runtime/vm.zig b/src/runtime/vm.zig index 132321b4..aa4a75bf 100644 --- a/src/runtime/vm.zig +++ b/src/runtime/vm.zig @@ -2884,6 +2884,7 @@ pub const VM = struct { pub fn interpret(self: *VM, result: *const CompileResult) RuntimeError!void { self.installHooks(); + try @import("../ini_config.zig").applyToVm(self); try extension.beginRequest(self); try self.registerResultFunctions(result); for (result.type_hints.items) |th| { diff --git a/src/stdlib/system.zig b/src/stdlib/system.zig index 93c8a9c6..79440f43 100644 --- a/src/stdlib/system.zig +++ b/src/stdlib/system.zig @@ -594,10 +594,9 @@ fn native_getcwd(ctx: *NativeContext, _: []const Value) RuntimeError!NativeResul return try NativeResult.copyString(ctx.allocator, cwd); } -fn native_php_ini_loaded_file(_: *NativeContext, _: []const Value) RuntimeError!NativeResult { - // zphp doesn't read a php.ini file; report false to match PHP behavior - // when no ini was loaded - return NativeResult.scalar(.{ .bool = false }); +fn native_php_ini_loaded_file(ctx: *NativeContext, _: []const Value) RuntimeError!NativeResult { + const path = @import("../ini_config.zig").loadedFile() orelse return NativeResult.scalar(.{ .bool = false }); + return NativeResult.copyString(ctx.allocator, path); } fn native_php_ini_scanned_files(_: *NativeContext, _: []const Value) RuntimeError!NativeResult { diff --git a/src/stdlib/types.zig b/src/stdlib/types.zig index 07696099..2ba0c214 100644 --- a/src/stdlib/types.zig +++ b/src/stdlib/types.zig @@ -1951,7 +1951,15 @@ fn native_ini_get_all(ctx: *NativeContext, _: []const Value) RuntimeError!Native return NativeResult.borrowed(.{ .array = try ctx.createArray() }); } -fn native_ini_restore(_: *NativeContext, _: []const Value) RuntimeError!NativeResult { +fn native_ini_restore(ctx: *NativeContext, args: []const Value) RuntimeError!NativeResult { + if (args.len == 0 or args[0] != .string) return NativeResult.scalar(.null); + const name = args[0].string.bytes(); + _ = ctx.vm.ini_settings.remove(name); + if (@import("../ini_config.zig").get(name)) |configured| { + const owned_name = try ctx.allocator.dupe(u8, name); + try ctx.vm.strings.append(ctx.allocator, owned_name); + try ctx.vm.ini_settings.put(ctx.allocator, owned_name, configured); + } return NativeResult.scalar(.null); } diff --git a/tests/ini/app.php b/tests/ini/app.php new file mode 100644 index 00000000..d8a79116 --- /dev/null +++ b/tests/ini/app.php @@ -0,0 +1,8 @@ + php_ini_loaded_file() !== false, + "tz" => ini_get("date.timezone"), + "tz_default" => date_default_timezone_get(), + "tz_date" => date("e"), + "error_reporting" => ini_get("error_reporting"), + "error_reporting_live" => error_reporting(), + "display_errors" => ini_get("display_errors"), + "memory_limit" => ini_get("memory_limit"), + "max_execution_time" => ini_get("max_execution_time"), + "name" => ini_get("app.name"), + "flag" => ini_get("app.flag"), + "quoted" => ini_get("app.quoted"), + "missing" => ini_get("app.missing"), +]), "\n"; diff --git a/tests/ini/restore.php b/tests/ini/restore.php new file mode 100644 index 00000000..781b5533 --- /dev/null +++ b/tests/ini/restore.php @@ -0,0 +1,5 @@ +/dev/null; true' EXIT +export ZPHP_INI_TEST_NAME=probe +case "$(uname -s)" in Darwin) LIB=dylib ;; *) LIB=so ;; esac +if [ "$(uname -s)" = Darwin ] && [ -z "${DEVELOPER_DIR:-}" ] && [ -d /Library/Developer/CommandLineTools ]; then + export DEVELOPER_DIR=/Library/Developer/CommandLineTools +fi + +passed=0 +failed=0 +check() { + local name="$1" expected="$2" actual="$3" + if [ "$expected" = "$actual" ]; then passed=$((passed + 1)); else + failed=$((failed + 1)); printf 'FAIL %s\n expected: %s\n actual: %s\n' "$name" "$expected" "$actual"; fi +} +contains() { + local name="$1" needle="$2" haystack="$3" + if [[ "$haystack" == *"$needle"* ]]; then passed=$((passed + 1)); else + failed=$((failed + 1)); printf 'FAIL %s\n expected to contain: %s\n actual: %s\n' "$name" "$needle" "$haystack"; fi +} + +expected='{"loaded":true,"tz":"America\/New_York","tz_default":"America\/New_York","tz_date":"America\/New_York","error_reporting":"22527","error_reporting_live":22527,"display_errors":"","memory_limit":"256M","max_execution_time":"30","name":"probe-app","flag":"1","quoted":"a;b","missing":false}' +check "--ini=PATH" "$expected" "$("$ZPHP" --ini="$INI" run "$SCRIPT_DIR/ini/probe.php")" +check "--ini PATH" "$expected" "$("$ZPHP" --ini "$INI" run "$SCRIPT_DIR/ini/probe.php")" +check "ZPHP_INI" "$expected" "$(ZPHP_INI="$INI" "$ZPHP" run "$SCRIPT_DIR/ini/probe.php")" +cp "$INI" "$OUT/php.ini" +check "php.ini in the working directory" "$expected" "$(cd "$OUT" && "$ZPHP" run "$SCRIPT_DIR/ini/probe.php")" +contains "no ini file" '"loaded":false,"tz":"UTC"' "$("$ZPHP" run "$SCRIPT_DIR/ini/probe.php")" +contains "-d overrides the file" '"tz":"Europe\/Paris","tz_default":"Europe\/Paris"' "$("$ZPHP" --ini="$INI" -d date.timezone=Europe/Paris run "$SCRIPT_DIR/ini/probe.php")" +contains "-d name=value form" '"memory_limit":"1G"' "$("$ZPHP" --ini="$INI" -d memory_limit=1G run "$SCRIPT_DIR/ini/probe.php")" +contains "-d without a file" '"loaded":false,"tz":"Asia\/Tokyo"' "$("$ZPHP" -d date.timezone=Asia/Tokyo run "$SCRIPT_DIR/ini/probe.php")" +check "ini_restore returns to the file value" "512M|256M" "$("$ZPHP" --ini="$INI" run "$SCRIPT_DIR/ini/restore.php")" +out="$("$ZPHP" --ini="$OUT/missing.ini" run "$SCRIPT_DIR/ini/probe.php" 2>&1)" && status=0 || status=$? +check "missing explicit file exits 1" "1" "$status" +contains "missing explicit file message" "ini file" "$out" +printf 'error_reporting = E_ALL &\n' > "$OUT/bad.ini" +contains "an unparseable level expression stays a string" '"error_reporting":"E_ALL &"' "$("$ZPHP" --ini="$OUT/bad.ini" run "$SCRIPT_DIR/ini/probe.php")" + +# serve: ini_set and date_default_timezone_set in one request never reach the next +"$ZPHP" --ini="$INI" serve "$SCRIPT_DIR/ini/app.php" --port "$PORT" --workers 1 >"$OUT/serve.log" 2>&1 & +SERVER_PID=$! +for _ in $(seq 1 50); do curl -s -o /dev/null "http://localhost:$PORT/health" 2>/dev/null && break; sleep 0.1; done +get() { curl -s "http://localhost:$PORT/$1"; } +check "serve probe" "256M|America/New_York|probe-app" "$(get probe)" +check "serve dirty request" "1G|Asia/Tokyo" "$(get dirty)" +check "serve probe after dirty" "256M|America/New_York|probe-app" "$(get probe)" +for _ in $(seq 1 10); do get dirty >/dev/null; done +check "serve probe after 10 dirty requests" "256M|America/New_York|probe-app" "$(get probe)" +kill "$SERVER_PID" 2>/dev/null || true; wait "$SERVER_PID" 2>/dev/null || true; SERVER_PID="" + +# extension= lines: a path, and a bare name resolved against extension_dir +zig cc -shared -O2 -I "$ROOT/include" -o "$OUT/demo.$LIB" "$ROOT/tests/extensions/demo.c" +printf 'extension = %s\ndemo.greeting = salut\n' "$OUT/demo.$LIB" > "$OUT/ext-path.ini" +check "extension= by path" 'string(11) "salut, ini!"' "$("$ZPHP" --ini="$OUT/ext-path.ini" run <(echo '&1 | tail -1)" +mkdir -p "$OUT/extdir" && cp "$OUT/demo.$LIB" "$OUT/extdir/" +printf 'extension_dir = %s\nextension = demo\n' "$OUT/extdir" > "$OUT/ext-name.ini" +check "extension= by name in extension_dir" 'bool(true)' "$("$ZPHP" --ini="$OUT/ext-name.ini" run <(echo '&1 | tail -1)" +printf 'extension = demo\n' > "$OUT/extdir/php.ini" +check "extension= by name next to the ini file" 'bool(true)' "$("$ZPHP" --ini="$OUT/extdir/php.ini" run <(echo '&1 | tail -1)" + +echo "$passed passed, $failed failed" +[ "$failed" -eq 0 ]