GitNexus/gitnexus/test/unit/lbug-native-safe-path.test.ts
Gergő Magyar d4449b4ec8
fix(lbug): resolve non-ASCII paths for KuzuDB on Windows (#1811) (#1817)
* fix(lbug): resolve non-ASCII paths to 8.3 short form on Windows (#1811)

KuzuDB's native C++ layer uses ANSI file APIs (fopen) on Windows.
When the repo path contains CJK or other non-ASCII characters, the
UTF-8 bytes from Node.js are misinterpreted as the system's Active
Code Page (e.g. GBK), producing a garbled path — "Error 3: The
system cannot find the path specified."

Add `toNativeSafePath()` which converts non-ASCII paths to their
Windows 8.3 short-name form (all-ASCII) before passing them to the
native layer. Applied to both the database open path and the COPY
CSV paths. No-ops on non-Windows and on all-ASCII paths.

Closes #1811

* test(lbug): add unit + integration tests for non-ASCII path handling (#1811)

- Unit tests for toNativeSafePath: ASCII passthrough, non-Windows
  no-op, Windows short-path conversion, nonexistent-path fallback
- Integration test: full initLbug + loadGraphToLbug round-trip with
  CJK characters in the storage path — runs on all platforms
- Fix toNativeSafePath to reject cmd.exe output containing '?' chars
  (replacement for unrepresentable Unicode in the console code page)
- Register integration test in vitest lbug-db project and
  cross-platform-tests.ts matrix

* chore(autofix): apply prettier + eslint fixes via /autofix command

* feat(lbug): junction fallback, tmpdir CSV staging, pool-adapter coverage (#1811)

U1+U4: toNativeSafePath now tries 8.3 short path → NTFS junction
fallback → diagnostic warning. Junctions target path.dirname(p) and
reconstruct the leaf. Handles EEXIST races. Registers cleanup on
exit/SIGTERM/SIGINT. Orphan scan on first call removes stale
junctions from prior crashes.

U2: loadGraphToLbug redirects csvDir to os.tmpdir() when
storagePath contains non-ASCII on Windows, avoiding non-ASCII
characters in COPY FROM paths entirely.

U3: All 4 createLbugDatabase call sites in pool-adapter.ts now
wrap dbPath with toNativeSafePath.

* fix(test): fix CI failures from toNativeSafePath addition (#1811)

- Fix lbug-non-ascii-path integration test: use CodeRelation (actual
  relationship table name) instead of CALLS
- Add toNativeSafePath to lbug-config.js mocks in pool-wal-recovery
  and lbug-pool-win-fts-probe tests — pool-adapter now imports it

* fix(lbug): sanitize path before cmd.exe shell expansion (CodeQL)

Reject paths containing cmd.exe metacharacters (" % | & < > ^)
before interpolating into the `for %I` short-path command.
Prevents command injection via crafted path names.

* fix(lbug): address code review findings in non-ASCII path implementation

- U1: Use process.exit(0) on Windows instead of process.kill re-raise
  (SIGTERM forcefully kills on Windows, handlers never fire)
- U2: Pass safePath to openWithLockRetry so sidecar sweep targets the
  path KuzuDB actually opened, not the original non-ASCII path
- U3: Skip junction creation in worker threads (isMainThread guard) to
  prevent junction leaks from pool-adapter workers
- U4: Replace existsSync with lstatSync in orphan scan to avoid 30s
  blocking on unreachable UNC network targets

* chore(autofix): apply prettier + eslint fixes via /autofix command

* fix(lbug): correct SIGTERM exit code and run Prettier (#1811)

- Use exit code 143 (SIGTERM) / 130 (SIGINT) on Windows instead of 0
  so termination is not masked as success
- Run Prettier to fix formatting (CI Gate blocker)

* fix(lbug): eliminate CodeQL command-injection taint in tryShortPath

Pass the path via GITNEXUS_SP environment variable instead of
interpolating it into the cmd.exe command string. The FOR loop
reads %GITNEXUS_SP% from the environment, so the command text is
entirely static — no user-controlled data in the shell command.

Also removes CMD_UNSAFE_RE since the env var approach makes
character-level sanitization unnecessary.

---------

Co-authored-by: Test <test@example.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-25 21:28:12 +01:00

63 lines
2.3 KiB
TypeScript

/**
* Unit Tests: toNativeSafePath
*
* Verifies the Windows non-ASCII path workaround that converts paths to
* 8.3 short-name form before passing them to KuzuDB's native layer.
*/
import { describe, it, expect } from 'vitest';
import { toNativeSafePath, cleanupNativePathJunctions } from '../../src/core/lbug/lbug-config.js';
describe('toNativeSafePath', () => {
it('returns ASCII paths unchanged on any platform', () => {
const p = 'C:\\Users\\test\\project\\.gitnexus\\lbug';
expect(toNativeSafePath(p)).toBe(p);
});
it('returns forward-slash ASCII paths unchanged', () => {
const p = '/home/user/project/.gitnexus/lbug';
expect(toNativeSafePath(p)).toBe(p);
});
it('returns empty string unchanged', () => {
expect(toNativeSafePath('')).toBe('');
});
if (process.platform !== 'win32') {
it('returns non-ASCII paths unchanged on non-Windows', () => {
const p = '/home/用户/project/.gitnexus/lbug';
expect(toNativeSafePath(p)).toBe(p);
});
}
if (process.platform === 'win32') {
it('converts a path with non-ASCII parent directory to an ASCII-safe form', () => {
// Real-world scenario: repo at C:\Project\中文\code, leaf is ASCII (.gitnexus/lbug).
// Create a CJK parent dir with an ASCII leaf to match.
const os = require('os');
const fs = require('fs');
const path = require('path');
const cjkParent = path.join(os.tmpdir(), `gn-safepath-测试-${Date.now()}`);
const asciiLeaf = path.join(cjkParent, 'lbug');
fs.mkdirSync(cjkParent, { recursive: true });
try {
const result = toNativeSafePath(asciiLeaf);
expect(typeof result).toBe('string');
expect(result.length).toBeGreaterThan(0);
// Either 8.3 short path or junction — both produce an all-ASCII result
// since the leaf ('lbug') is ASCII and the parent is resolved
if (result !== asciiLeaf) {
expect(/^[\x00-\x7F]+$/.test(result)).toBe(true);
}
} finally {
cleanupNativePathJunctions();
fs.rmSync(cjkParent, { recursive: true, force: true });
}
});
it('returns the original path when the target does not exist', () => {
const nonexistent = 'C:\\不存在的路径\\test';
const result = toNativeSafePath(nonexistent);
expect(result).toBe(nonexistent);
});
}
});