Skip to content

feat(windows): core I/O — file locking and path separators#171

Open
JF10R wants to merge 6 commits intojustrach:mainfrom
JF10R:windows/core-io
Open

feat(windows): core I/O — file locking and path separators#171
JF10R wants to merge 6 commits intojustrach:mainfrom
JF10R:windows/core-io

Conversation

@JF10R
Copy link
Copy Markdown

@JF10R JF10R commented Apr 6, 2026

Closes #182
Depends on #170

Summary

Adds Windows-compatible file I/O: advisory locking via NT APIs, backslash-aware path handling, and cross-platform file truncation.

Changes

File Change
src/store.zig NtLockFile/NtUnlockFile advisory locking on Windows (POSIX flock on others); check NTSTATUS before unlocking
src/watcher.zig isSep() helper for both / and \; backslash-aware shouldSkip(); %TEMP%/%TMP% notify path on Windows; backslash-to-forward-slash normalization in drainNotifyFile; cross-platform setEndPos() replacing ftruncate

Tests

zig build test — 252/258 passed, 2 failed (pre-existing), 4 skipped (POSIX-only)

Same pass rate as foundation PR — no regressions.

Before/After

  • Before: flock() calls fail on Windows (POSIX-only). Paths with \ separators bypass shouldSkip(). Notify file path hardcodes /tmp/.
  • After: Advisory locking uses NtLockFile on Windows. All path operations handle both separators. Notify file uses %TEMP%/%TMP% on Windows.

Compliance

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7a078963f7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

src/watcher.zig Outdated
Comment on lines 701 to 703
const rel = if (std.mem.startsWith(u8, path, root))
std.mem.trimLeft(u8, path[root.len..], "/")
std.mem.trimLeft(u8, path[root.len..], "/\\")
else
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Normalize notified Windows paths before indexing

drainNotifyFile now accepts Windows-style \ paths but forwards rel unchanged into indexFileContent. The downstream sensitive-file guard (shouldSkipFile) still parses basenames and sensitive directories using /-based checks (e.g. .ssh/), so notified paths like .ssh\id_rsa can bypass exclusion and get indexed/searched on Windows. This is a security regression for the new Windows notify flow and should be fixed by normalizing separators (or making shouldSkipFile separator-agnostic) before filtering/indexing.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds core Windows I/O support needed for watcher/store functionality by addressing Windows path separator behavior, stack constraints, and cross-platform file operations/locking.

Changes:

  • Introduces compat.path_buf_size and updates multiple path buffers to use it on Windows.
  • Heap-allocates large data structures (watcher.EventQueue, frequency-table working buffers) to avoid Windows stack overflow.
  • Improves Windows filesystem interoperability: \-aware directory skipping, %TEMP%/%TMP% notify path, setEndPos() truncation, and Windows advisory locking via NtLockFile/NtUnlockFile.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/watcher.zig Heap-allocate event queue; add separator helper; make skip + notify draining Windows-aware; use setEndPos()
src/tests.zig Update watcher queue tests to use EventQueue.init()/deinit()
src/store.zig Add Windows data.log locking using NtLockFile/NtUnlockFile; use flock elsewhere
src/main.zig Switch to noinline mainImpl; use compat.path_buf_size; pass queue by pointer after new init
src/index.zig Move large frequency-table buffers to heap; adjust finishFrequencyTable to write into provided output
src/compat.zig Add path_buf_size to cap Windows path buffers for stack safety
build.zig Set Windows executable stack size to 8MB
Comments suppressed due to low confidence (1)

src/main.zig:548

  • resolveRoot now takes a *[compat.path_buf_size]u8 buffer, which caps resolvable absolute root path length to 1024 bytes on Windows. If the intent is only to avoid huge stack buffers in hot/replicated structs (like FsEvent/EventQueue), consider leaving resolveRoot on std.fs.max_path_bytes (or using a dynamically sized buffer) so long Windows roots remain supported.
fn resolveRoot(root: []const u8, buf: *[compat.path_buf_size]u8) ![]const u8 {
    if (std.mem.eql(u8, root, ".")) {
        return std.fs.cwd().realpath(".", buf) catch return error.ResolveFailed;
    }
    return std.fs.cwd().realpath(root, buf) catch return error.ResolveFailed;

src/store.zig Outdated
Comment on lines +83 to +97
if (comptime @import("builtin").os.tag == .windows) {
const ntdll = std.os.windows.ntdll;
const max_off: i64 = std.math.maxInt(i64);
const zero_off: i64 = 0;
var iosb: std.os.windows.IO_STATUS_BLOCK = undefined;
_ = ntdll.NtLockFile(log.handle, null, null, null, &iosb, &zero_off, &max_off, null, 0, 1);
defer {
var iosb2: std.os.windows.IO_STATUS_BLOCK = undefined;
_ = ntdll.NtUnlockFile(log.handle, &iosb2, &zero_off, &max_off, null);
}
} else {
const fd = log.handle;
_ = std.posix.flock(fd, std.posix.LOCK.EX) catch {};
defer _ = std.posix.flock(fd, std.posix.LOCK.UN) catch {};
}
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Windows branch calls NtLockFile/NtUnlockFile but ignores the returned NTSTATUS and IO_STATUS_BLOCK.Status. If the lock fails (or completes asynchronously), the code will still proceed to write under the assumption that cross-process safety was achieved. Consider checking the status (and only unlocking when the lock succeeded), and decide whether to fail the write or fall back to an unlocked write with an explicit warning when locking is unavailable.

Copilot uses AI. Check for mistakes.
}

var root_buf: [std.fs.max_path_bytes]u8 = undefined;
var root_buf: [compat.path_buf_size]u8 = undefined;
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using compat.path_buf_size (1024 on Windows) for the realpath buffer means roots with absolute paths longer than 1024 bytes will now fail to resolve on Windows, even though std.fs.max_path_bytes supports much longer paths there. Since this buffer isn’t multiplied into large stack allocations like FsEvent/EventQueue, consider keeping std.fs.max_path_bytes here (or using a heap allocation on Windows) to avoid unnecessarily limiting valid project roots.

Suggested change
var root_buf: [compat.path_buf_size]u8 = undefined;
var root_buf: [std.fs.max_path_bytes]u8 = undefined;

Copilot uses AI. Check for mistakes.
@JF10R JF10R force-pushed the windows/core-io branch 2 times, most recently from ffa3705 to ef0ef34 Compare April 6, 2026 15:05
@JF10R
Copy link
Copy Markdown
Author

JF10R commented Apr 6, 2026

Addressing reviewer feedback

✅ Fixed: watcher.zig:703 — normalize notified Windows paths (P1)

Added backslash-to-forward-slash normalization in drainNotifyFile on Windows. External tools (editors, scripts) may write \-separated paths to the notify file, but the walker stores paths with /. Without normalization, the same file could get duplicate entries in the explorer. (commit ef0ef34)

✅ Fixed: store.zig:97 — NtLockFile return status ignored

Now checks NTSTATUS == .SUCCESS and only unlocks if the lock was actually acquired. If the lock fails, writes still proceed (advisory lock — best-effort, same as upstream's POSIX flock pattern which also catches and ignores errors). The key improvement is we no longer call NtUnlockFile on a lock we don't hold. (commit ef0ef34)

Won't fix: main.zig:114resolveRoot buffer size

Same reasoning as explained on #170 — 1024 bytes covers all realistic project root paths. Extended \?\ paths reaching 32K are not used for project roots.

@justrach
Copy link
Copy Markdown
Owner

justrach commented Apr 6, 2026

Hey @JF10R — thanks for the Windows work! Before we can review and merge, could you please:

  1. Rebase onto current main — we've landed several changes (mmap trigram index, fuzzy search, process lifecycle fixes, root policy updates) that may conflict
  2. Follow the requirements in CONTRIBUTING.md — each PR needs:
    • Linked issue number
    • Summary of exact changes + files touched
    • Tests run (include zig build test output)
    • Failing test before fix / passing test after
    • Confirmation branch is rebased onto current main
    • No generated artifacts committed
  3. Keep PRs tightly scoped — the foundation → I/O → MCP → security chain looks good, just make sure each one compiles independently after rebase

We'll get Codex to do a review pass once rebased. Thanks!

Copy link
Copy Markdown
Owner

@justrach justrach left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting rebase onto current main and CONTRIBUTING.md compliance before review. See comment above.

JF10R added 6 commits April 6, 2026 19:37
Add Windows platform support foundations that all subsequent Windows PRs
depend on. No behavioral change on Linux/macOS.

- compat.zig: add path_buf_size (1024 on Windows vs max_path_bytes)
- build.zig: set 8MB stack for Windows (matches Linux default)
- main.zig: replace thread trampoline with noinline mainImpl to avoid
  LLVM frame merging (~128MB) that exceeds Windows guard page stride
- main.zig: fix double scan_thread.join() (UB on all platforms)
- watcher.zig: heap-allocate EventQueue (avoids ~4MB stack array)
- watcher.zig: use compat.path_buf_size in FsEvent
- index.zig: heap-allocate frequency table buffers (~640KB each)
- tests.zig: update EventQueue construction to use init()/deinit()
Upstream issue-148 added stdin HUP detection via std.posix.poll() and
pipe tests. These use POSIX APIs unavailable on Windows. Guard the poll
block with comptime os check (Windows uses idle timeout only) and skip
pipe tests on Windows.
Upstream's mmap trigram index (justrach#167) uses std.posix.mmap/munmap which
are unavailable on Windows. Guard initFromDisk to return null on Windows
(falling back to heap-based index) and skip mmap-dependent tests.
Platform-specific I/O changes for Windows file system operations.

- store.zig: use NtLockFile/NtUnlockFile on Windows (POSIX flock on others)
- watcher.zig: add isSep() to handle both / and \ path separators
- watcher.zig: backslash-aware shouldSkip() for Windows paths
- watcher.zig: use %TEMP%/%TMP% for codedb-notify path on Windows
- watcher.zig: use setEndPos() instead of ftruncate (cross-platform)
- watcher.zig: trim both / and \ when making paths relative to root
- store.zig: check NtLockFile return status; only unlock if lock
  succeeded; skip write-under-lock if lock fails (matches upstream's
  advisory lock pattern)
- watcher.zig: normalize backslash paths to forward slashes in
  drainNotifyFile to avoid duplicate entries in the explorer (walker
  uses / convention, notify file may contain \ on Windows)
@JF10R JF10R force-pushed the windows/core-io branch from ef0ef34 to 57a84be Compare April 6, 2026 23:45
@JF10R
Copy link
Copy Markdown
Author

JF10R commented Apr 6, 2026

Rebased onto current main (d7e3bfb), updated PR description per CONTRIBUTING.md.

zig build test output

$ zig build test
Build Summary: 5/7 steps succeeded; 1 failed; 297/303 tests passed; 4 skipped; 2 failed
  run test 252/258 passed, 2 failed, 4 skipped

Same pass rate as #170 — no regressions introduced.

2 failed (pre-existing — see #170 comment for proof):

  • issue-150: --help prints usage
  • issue-150: -h prints usage

4 skipped: same POSIX-only tests as #170.

Red-to-green

Before (without this PR, on Windows):

  • flock() in store.zigerror.Unexpected (POSIX-only API)
  • Paths with \ bypass shouldSkip() directory filtering
  • Notify file hardcodes /tmp/codedb-notify (doesn't exist on Windows)

After:

  • NtLockFile/NtUnlockFile used on Windows, NTSTATUS checked before unlock
  • isSep() helper handles both / and \
  • Notify path uses %TEMP%/%TMP% on Windows
  • drainNotifyFile normalizes \ to / before indexing

Nearby non-regression checks

All 252 passing tests are unchanged upstream tests. Specifically:

  • All watcher tests pass (including EventQueue init/deinit, path handling)
  • All store tests pass (advisory locking works on Windows via NT APIs)
  • All explorer search/index tests pass (path normalization doesn't affect POSIX paths)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Windows support — file locking and path separators

3 participants