@robota-sdk/agent-file-authority 3.0.0-beta.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +22 -0
- package/dist/node/index.cjs +1 -0
- package/dist/node/index.d.cts +26 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +26 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +2 -0
- package/dist/node/index.js.map +1 -0
- package/package.json +76 -0
- package/src/__tests__/stable-rooted-file-reader.test.ts +240 -0
- package/src/__tests__/windows-native-authority.test.ts +113 -0
- package/src/contracts.ts +54 -0
- package/src/host-backend.ts +13 -0
- package/src/index.ts +6 -0
- package/src/posix-backend.ts +253 -0
- package/src/stable-rooted-file-reader.ts +124 -0
- package/src/windows-backend.ts +89 -0
- package/src/windows-native-api.ts +234 -0
- package/src/windows-native-authority.ts +227 -0
- package/src/windows-native-read.ts +122 -0
- package/src/windows-native-status.ts +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# @robota-sdk/agent-file-authority
|
|
2
|
+
|
|
3
|
+
Opaque, stable, root-relative bounded file reads for supported Node.js hosts. The package retains a
|
|
4
|
+
native directory authority and opens each requested path segment relative to that authority, so a
|
|
5
|
+
symlink, junction, or pathname replacement cannot redirect an in-progress read.
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { createStableRootedFileReader } from '@robota-sdk/agent-file-authority';
|
|
9
|
+
|
|
10
|
+
using reader = createStableRootedFileReader('/approved/root');
|
|
11
|
+
const bytes = reader.readBytes(['payloads', 'record.json'], 1_048_576);
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`readBytes()` accepts validated single path segments and a mandatory byte limit. It returns
|
|
15
|
+
`undefined` only for a missing entry; unsafe entries, unsupported hosts, changed files, and exceeded
|
|
16
|
+
budgets are explicit `StableFileAuthorityError` results. Raw descriptors, Windows handles, canonical
|
|
17
|
+
paths, and pathname fallbacks are never exposed.
|
|
18
|
+
|
|
19
|
+
The qualified native targets are Linux x64/arm64, macOS x64/arm64, and Windows x64 on Node.js
|
|
20
|
+
20.19 or newer. Other targets fail closed with `UNSUPPORTED_BACKEND`.
|
|
21
|
+
|
|
22
|
+
See [the package specification](./docs/SPEC.md) for the contract and security boundaries.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));let c=require("node:path"),l=require("node:fs"),u=require("koffi");u=s(u,1);const d={INVALID_PATH:`The requested root-relative file operation is invalid.`,UNSAFE_ENTRY:`The requested entry cannot be opened without following an unsafe object.`,UNSUPPORTED_BACKEND:`This host cannot provide stable root-relative file reads.`,AUTHORITY_CLOSED:`The stable rooted file authority is closed.`,ROOT_CHANGED:`The retained root identity changed during the operation.`,FILE_CHANGED:`The file identity or contents changed during the bounded read.`,OVER_BUDGET:`The file exceeds the supplied byte budget.`,HOST_IO:`The host could not complete the stable root-relative file operation.`};var f=class extends Error{code;context;constructor(e,t){super(d[e]),this.name=`StableFileAuthorityError`,this.code=e,this.context=Object.freeze({...t})}};function p(e,t,n={}){return new f(e,{operation:t,...n})}function m(e){return{device:e.dev,inode:e.ino,mode:e.mode}}function h(e){return{...m(e),size:e.size,modified:e.mtimeNs,changed:e.ctimeNs}}function g(e,t){return e.device===t.device&&e.inode===t.inode&&e.mode===t.mode}function _(e,t){return g(e,t)&&e.size===t.size&&e.modified===t.modified&&e.changed===t.changed}function v(e){for(let[t,n]of Object.entries(u.os.errno))if(n===e)return t;return`ERRNO_${e}`}function y(e){let t=l.constants.O_CLOEXEC??(process.platform===`darwin`?16777216:524288);return l.constants.O_RDONLY|l.constants.O_NOFOLLOW|l.constants.O_NONBLOCK|t|(e?l.constants.O_DIRECTORY:0)}function b(e,t){try{return(0,l.fstatSync)(e,{bigint:!0})}catch(e){let n=x(typeof e==`object`?e:null);throw p(`HOST_IO`,t,n===void 0?{}:{hostCode:n})}}function x(e){if(!(e===null||!(`code`in e)))return typeof e.code==`string`?e.code:void 0}var S=class{openAt;rootIdentity;rootDescriptor;constructor(e){try{let t=u.load(process.platform===`darwin`?`/usr/lib/libSystem.B.dylib`:`libc.so.6`),n=t.func(`int open(const char *path, int flags)`);this.openAt=t.func(`int openat(int fd, const char *path, int flags)`);let r=n(e,y(!0));r<0&&this.throwOpenFailure(u.errno(),`open-root`),this.rootDescriptor=r;let i=b(r,`inspect-root`);if(!i.isDirectory())throw p(`UNSAFE_ENTRY`,`open-root`);this.rootIdentity=m(i)}catch(e){throw this.rootDescriptor!==void 0&&(this.closeDescriptor(this.rootDescriptor),this.rootDescriptor=void 0),e instanceof f?e:p(`UNSUPPORTED_BACKEND`,`load-posix-backend`)}}readBytes(e,t,n){let r=this.rootDescriptor;if(r===void 0)throw p(`AUTHORITY_CLOSED`,`read`);this.assertRootIdentity(r);let i=r,a=!1,o;try{for(let t=0;t<e.length;t+=1){let r=t===e.length-1,s=this.openAt(i,e[t],y(!r));if(s<0){let e=u.errno();if(e===u.os.errno.ENOENT)return;this.throwOpenFailure(e,r?`open-file`:`open-directory`,t)}if(r){o=s;break}if(!b(s,`inspect-directory`).isDirectory())throw this.closeDescriptor(s),p(`UNSAFE_ENTRY`,`inspect-directory`,{segmentIndex:t});a&&this.closeDescriptor(i),i=s,a=!0,n.afterDirectoryOpened?.(t)}if(o===void 0)throw p(`HOST_IO`,`open-file`);let s=this.readFile(o,t,n);return this.assertRootIdentity(r),s}catch(e){if(e instanceof f)throw e;let t=x(typeof e==`object`?e:null);throw p(`HOST_IO`,`read`,t===void 0?{}:{hostCode:t})}finally{this.closeOptionalDescriptor(o),this.closeOptionalDescriptor(a?i:void 0)}}close(){let e=this.rootDescriptor;e!==void 0&&(this.rootDescriptor=void 0,this.closeDescriptor(e))}readFile(e,t,n){let r=b(e,`inspect-file`);if(!r.isFile())throw p(`UNSAFE_ENTRY`,`inspect-file`);if(r.size>BigInt(t))throw p(`OVER_BUDGET`,`read-file`);let i=Number(r.size),a;try{a=Buffer.alloc(i)}catch{throw p(`HOST_IO`,`allocate-read-buffer`)}let o=0;for(;o<i;){let t=(0,l.readSync)(e,a,o,Math.min(1048576,i-o),o);if(t===0)break;o+=t,n.afterReadChunk?.(o)}let s=(0,l.readSync)(e,Buffer.alloc(1),0,1,i)!==0,c=b(e,`reinspect-file`);if(s&&i>=t)throw p(`OVER_BUDGET`,`read-file`);if(o!==i||s||!_(h(r),h(c)))throw p(`FILE_CHANGED`,`read-file`);return a}assertRootIdentity(e){let t=b(e,`inspect-root`);if(!t.isDirectory()||!g(this.rootIdentity,m(t)))throw p(`ROOT_CHANGED`,`inspect-root`)}throwOpenFailure(e,t,n){let r=v(e),i=n===void 0?{hostCode:r}:{hostCode:r,segmentIndex:n};throw e===u.os.errno.ELOOP||e===u.os.errno.ENOTDIR||e===u.os.errno.EISDIR?p(`UNSAFE_ENTRY`,t,i):p(`HOST_IO`,t,i)}closeDescriptor(e){try{(0,l.closeSync)(e)}catch{}}closeOptionalDescriptor(e){e!==void 0&&this.closeDescriptor(e)}};function C(){let e=u.pointer(`RobotaStableFileHandle`,u.opaque()),t=u.struct(`RobotaUnicodeString`,{Length:`uint16_t`,MaximumLength:`uint16_t`,Buffer:u.pointer(`char16_t`)});return{handleType:e,unicodeStringType:t,objectAttributesType:u.struct(`RobotaObjectAttributes`,{Length:`uint32_t`,RootDirectory:e,ObjectName:u.pointer(t),Attributes:`uint32_t`,SecurityDescriptor:`void *`,SecurityQualityOfService:`void *`}),ioStatusBlockType:u.struct(`RobotaIoStatusBlock`,{Status:`intptr_t`,Information:`uintptr_t`}),attributeTagInfoType:u.struct(`RobotaFileAttributeTagInfo`,{FileAttributes:`uint32_t`,ReparseTag:`uint32_t`}),fileIdInfoType:u.struct(`RobotaFileIdInfo`,{VolumeSerialNumber:`uint64_t`,FileId:u.array(`uint8_t`,16,`Typed`)}),standardInfoType:u.struct(`RobotaFileStandardInfo`,{AllocationSize:`int64_t`,EndOfFile:`int64_t`,NumberOfLinks:`uint32_t`,DeletePending:`uint8_t`,Directory:`uint8_t`})}}function w(e,t){return{createFileW:e.func(`CreateFileW`,t.handleType,[u.pointer(`char16_t`),`uint32_t`,`uint32_t`,`void *`,`uint32_t`,`uint32_t`,t.handleType]),closeHandle:e.func(`CloseHandle`,`bool`,[t.handleType]),...T(e,t),getFileSizeEx:e.func(`GetFileSizeEx`,`bool`,[t.handleType,u.out(u.pointer(`int64_t`))]),readFile:e.func(`ReadFile`,`bool`,[t.handleType,u.out(u.pointer(`uint8_t`)),`uint32_t`,u.out(u.pointer(`uint32_t`)),`void *`]),getLastError:e.func(`GetLastError`,`uint32_t`,[])}}function T(e,t){let n=n=>e.func(`GetFileInformationByHandleEx`,`bool`,[t.handleType,`int32_t`,u.out(u.pointer(n)),`uint32_t`]);return{getAttributeTagInfo:n(t.attributeTagInfoType),getFileIdInfo:n(t.fileIdInfoType),getStandardInfo:n(t.standardInfoType)}}function E(e,t){return{ntCreateFile:e.func(`NtCreateFile`,`int32_t`,[u.out(u.pointer(t.handleType)),`uint32_t`,u.pointer(t.objectAttributesType),u.out(u.pointer(t.ioStatusBlockType)),`void *`,`uint32_t`,`uint32_t`,`uint32_t`,`uint32_t`,`void *`,`uint32_t`]),ntClose:e.func(`NtClose`,`int32_t`,[t.handleType])}}let D;function O(){if(D!==void 0)return D;let e=C();return D={...e,...w(u.load(`kernel32.dll`),e),...E(u.load(`ntdll.dll`),e)},D}const k=BigInt(`0`);function A(e){if(e instanceof Uint8Array||Array.isArray(e))return Buffer.from(e).toString(`hex`);throw p(`UNSUPPORTED_BACKEND`,`decode-file-identity`)}function j(e){return`WIN32_${e.getLastError()}`}function M(e,t,n){let r={};if(!e.getFileIdInfo(t,18,r,u.sizeof(e.fileIdInfoType)))throw p(`UNSUPPORTED_BACKEND`,n,{hostCode:j(e)});return{volume:BigInt(r.VolumeSerialNumber??k),fileId:A(r.FileId)}}function N(e,t,n,r){let i=F(e,t);if(i.size>BigInt(n))throw p(`OVER_BUDGET`,`read-file`);let a=Number(i.size),o;try{o=Buffer.alloc(a)}catch{throw p(`HOST_IO`,`allocate-read-buffer`)}let s=P(e,t,o,r),c=[0];if(!e.readFile(t,Buffer.alloc(1),1,c,null))throw p(`HOST_IO`,`probe-file`,{hostCode:j(e)});let l=F(e,t);if(c[0]!==0&&a>=n)throw p(`OVER_BUDGET`,`read-file`);if(!I(i,l,s,a,c[0]))throw p(`FILE_CHANGED`,`read-file`);return o}function P(e,t,n,r){let i=0;for(;i<n.length;){let a=Math.min(1048576,n.length-i),o=[0];if(!e.readFile(t,n.subarray(i,i+a),a,o,null))throw p(`HOST_IO`,`read-file`,{hostCode:j(e)});if(o[0]===0)break;i+=o[0],r.afterReadChunk?.(i)}return i}function F(e,t){let n=M(e,t,`inspect-file`),r=[k];if(!e.getFileSizeEx(t,r))throw p(`HOST_IO`,`inspect-file`,{hostCode:j(e)});return{...n,size:BigInt(r[0])}}function I(e,t,n,r,i){return n===r&&i===0&&e.volume===t.volume&&e.fileId===t.fileId&&e.size===t.size}const L=new Set([3221225487,3221225524,3221225530]),R=new Set([2147483693,3221225658,3221225731,3221226105,3221226763]);function z(e){let t=e>>>0;return{kind:L.has(t)?`missing`:R.has(t)?`unsafe`:`host-io`,hostCode:`NTSTATUS_0x${t.toString(16)}`}}const B=BigInt(`-1`);function V(e){return e===null||BigInt.asIntN(64,e)===B}var H=class{api;constructor(){try{this.api=O()}catch{throw p(`UNSUPPORTED_BACKEND`,`load-windows-backend`)}}openRoot(e){let t=this.api.createFileW(e,2147483648,7,null,3,35651584,null);if(V(t))throw p(`HOST_IO`,`open-root`,{hostCode:this.lastErrorCode()});try{return this.assertSafeHandle(t,!0,`inspect-root`),t}catch(e){throw this.close(t),e}}openRelative(e,t,n,r){let{handle:i,status:a}=this.issueRelativeOpen(e,t,n);if(a>=0){if(V(i))throw p(`HOST_IO`,`open-relative`);try{return this.assertSafeHandle(i,!n,n?`inspect-file`:`inspect-directory`,r),i}catch(e){throw this.close(i),e}}V(i)||this.close(i);let o=z(a);if(o.kind===`missing`)return;let s={segmentIndex:r,hostCode:o.hostCode};throw o.kind===`unsafe`?p(`UNSAFE_ENTRY`,n?`open-file`:`open-directory`,s):p(`HOST_IO`,n?`open-file`:`open-directory`,s)}readFile(e,t,n){return N(this.api,e,t,n)}queryIdentity(e,t){return M(this.api,e,t)}close(e){try{if(this.api.ntClose(e)>=0)return}catch{}try{this.api.closeHandle(e)}catch{}}issueRelativeOpen(e,t,n){let r=u.alloc(this.api.unicodeStringType,1);try{u.encode(r,this.api.unicodeStringType,{Length:Buffer.byteLength(t,`utf16le`),MaximumLength:Buffer.byteLength(`${t}\0`,`utf16le`),Buffer:t});let i={Length:u.sizeof(this.api.objectAttributesType),RootDirectory:e,ObjectName:r,Attributes:4160,SecurityDescriptor:null,SecurityQualityOfService:null},a=[null],o={Status:0,Information:0},s=n?5:7,c=this.api.ntCreateFile(a,1048705,i,o,null,0,s,1,(n?64:1)|2097184,null,0);return{handle:a[0],status:c}}finally{u.free(r)}}assertSafeHandle(e,t,n,r){let i={},a={};if(!this.api.getAttributeTagInfo(e,9,i,u.sizeof(this.api.attributeTagInfoType))||!this.api.getStandardInfo(e,1,a,u.sizeof(this.api.standardInfoType)))throw p(`HOST_IO`,n,{hostCode:this.lastErrorCode()});if((i.FileAttributes??0)&1024||!!a.Directory!==t)throw p(`UNSAFE_ENTRY`,n,r===void 0?{}:{segmentIndex:r})}lastErrorCode(){return j(this.api)}},U=class{native=new H;rootIdentity;rootHandle;constructor(e){try{let t=this.native.openRoot(e);this.rootHandle=t,this.rootIdentity=this.native.queryIdentity(t,`inspect-root`)}catch(e){throw this.rootHandle!==void 0&&(this.native.close(this.rootHandle),this.rootHandle=void 0),e instanceof f?e:p(`UNSUPPORTED_BACKEND`,`load-windows-backend`)}}readBytes(e,t,n){let r=this.rootHandle;if(r===void 0)throw p(`AUTHORITY_CLOSED`,`read`);this.assertRootIdentity(r);let i=r,a=!1,o;try{for(let t=0;t<e.length;t+=1){let r=t===e.length-1,s=this.native.openRelative(i,e[t],r,t);if(s===void 0)return;if(r){o=s;break}a&&this.native.close(i),i=s,a=!0,n.afterDirectoryOpened?.(t)}if(o===void 0)throw p(`HOST_IO`,`open-file`);let s=this.native.readFile(o,t,n);return this.assertRootIdentity(r),s}catch(e){throw e instanceof f?e:p(`HOST_IO`,`read`)}finally{o!==void 0&&this.native.close(o),a&&this.native.close(i)}}close(){let e=this.rootHandle;e!==void 0&&(this.rootHandle=void 0,this.native.close(e))}assertRootIdentity(e){let t=this.native.queryIdentity(e,`inspect-root`);if(t.volume!==this.rootIdentity.volume||t.fileId!==this.rootIdentity.fileId)throw p(`ROOT_CHANGED`,`inspect-root`)}};const W=new Set([`darwin-arm64`,`darwin-x64`,`linux-arm64`,`linux-x64`,`win32-x64`]),G=new FinalizationRegistry(e=>{e.close()});function K(e){if(typeof e!=`string`||e.trim().length===0||e.includes(`\0`))throw p(`INVALID_PATH`,`validate-root`);return(0,c.resolve)(e)}function q(e,t){if(!Array.isArray(e)||e.length===0)throw p(`INVALID_PATH`,`validate-segments`);for(let t=0;t<e.length;t+=1){let n=e[t];if(typeof n!=`string`||n.length===0||n===`.`||n===`..`||n.includes(`\0`)||n.includes(`/`)||n.includes(`\\`))throw p(`INVALID_PATH`,`validate-segments`,{segmentIndex:t})}if(!Number.isSafeInteger(t)||t<0)throw p(`INVALID_PATH`,`validate-budget`)}function J(e){let t=`${process.platform}-${process.arch}`;if(!W.has(t))throw p(`UNSUPPORTED_BACKEND`,`select-backend`,{hostCode:t});return process.platform===`win32`?new U(e):new S(e)}var Y=class{hooks;backend;finalizerToken={};constructor(e,t){this.hooks=t;let n=J(K(e));this.backend=n,G.register(this,n,this.finalizerToken)}readBytes(e,t){let n=this.backend;if(n===void 0)throw p(`AUTHORITY_CLOSED`,`read`);q(e,t);try{return n.readBytes(e,t,this.hooks)}catch(e){throw e instanceof f?e:p(`HOST_IO`,`read`)}}close(){let e=this.backend;if(e!==void 0){this.backend=void 0,G.unregister(this.finalizerToken);try{e.close()}catch(e){throw e instanceof f?e:p(`HOST_IO`,`close`)}}}[Symbol.dispose](){this.close()}};function X(e){return new Y(e,{})}exports.StableFileAuthorityError=f,exports.createStableRootedFileReader=X;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region src/contracts.d.ts
|
|
2
|
+
type TStableFileAuthorityErrorCode = 'INVALID_PATH' | 'UNSAFE_ENTRY' | 'UNSUPPORTED_BACKEND' | 'AUTHORITY_CLOSED' | 'ROOT_CHANGED' | 'FILE_CHANGED' | 'OVER_BUDGET' | 'HOST_IO';
|
|
3
|
+
interface IStableFileAuthorityErrorContext {
|
|
4
|
+
readonly operation: string;
|
|
5
|
+
readonly segmentIndex?: number;
|
|
6
|
+
readonly hostCode?: string;
|
|
7
|
+
}
|
|
8
|
+
/** A stable, secret-free failure from a rooted file authority. */
|
|
9
|
+
declare class StableFileAuthorityError extends Error {
|
|
10
|
+
readonly code: TStableFileAuthorityErrorCode;
|
|
11
|
+
readonly context: Readonly<IStableFileAuthorityErrorContext>;
|
|
12
|
+
constructor(code: TStableFileAuthorityErrorCode, context: IStableFileAuthorityErrorContext);
|
|
13
|
+
}
|
|
14
|
+
/** Opaque authority for bounded regular-file reads beneath one retained directory identity. */
|
|
15
|
+
interface IStableRootedFileReader {
|
|
16
|
+
readBytes(relativeSegments: readonly string[], maxBytes: number): Uint8Array | undefined;
|
|
17
|
+
close(): void;
|
|
18
|
+
[Symbol.dispose](): void;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/stable-rooted-file-reader.d.ts
|
|
22
|
+
/** Create a stable, opaque read authority rooted at the currently opened directory identity. */
|
|
23
|
+
declare function createStableRootedFileReader(rootDirectory: string): IStableRootedFileReader;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { type IStableRootedFileReader, StableFileAuthorityError, type TStableFileAuthorityErrorCode, createStableRootedFileReader };
|
|
26
|
+
//# sourceMappingURL=index.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/contracts.ts","../../src/stable-rooted-file-reader.ts"],"mappings":";KAAY;UAUK;WACN;WACA;WACA;;;cAeE,iCAAiC;WACnC,MAAM;WACN,SAAS,SAAS;EAEf,YAAA,MAAM,+BAA+B,SAAS;;;UAS3C;EACf,UAAU,qCAAqC,mBAAmB;EAClE;GACC,OAAO;;;;;iBCqEM,6BAA6B,wBAAwB"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region src/contracts.d.ts
|
|
2
|
+
type TStableFileAuthorityErrorCode = 'INVALID_PATH' | 'UNSAFE_ENTRY' | 'UNSUPPORTED_BACKEND' | 'AUTHORITY_CLOSED' | 'ROOT_CHANGED' | 'FILE_CHANGED' | 'OVER_BUDGET' | 'HOST_IO';
|
|
3
|
+
interface IStableFileAuthorityErrorContext {
|
|
4
|
+
readonly operation: string;
|
|
5
|
+
readonly segmentIndex?: number;
|
|
6
|
+
readonly hostCode?: string;
|
|
7
|
+
}
|
|
8
|
+
/** A stable, secret-free failure from a rooted file authority. */
|
|
9
|
+
declare class StableFileAuthorityError extends Error {
|
|
10
|
+
readonly code: TStableFileAuthorityErrorCode;
|
|
11
|
+
readonly context: Readonly<IStableFileAuthorityErrorContext>;
|
|
12
|
+
constructor(code: TStableFileAuthorityErrorCode, context: IStableFileAuthorityErrorContext);
|
|
13
|
+
}
|
|
14
|
+
/** Opaque authority for bounded regular-file reads beneath one retained directory identity. */
|
|
15
|
+
interface IStableRootedFileReader {
|
|
16
|
+
readBytes(relativeSegments: readonly string[], maxBytes: number): Uint8Array | undefined;
|
|
17
|
+
close(): void;
|
|
18
|
+
[Symbol.dispose](): void;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/stable-rooted-file-reader.d.ts
|
|
22
|
+
/** Create a stable, opaque read authority rooted at the currently opened directory identity. */
|
|
23
|
+
declare function createStableRootedFileReader(rootDirectory: string): IStableRootedFileReader;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { type IStableRootedFileReader, StableFileAuthorityError, type TStableFileAuthorityErrorCode, createStableRootedFileReader };
|
|
26
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/contracts.ts","../../src/stable-rooted-file-reader.ts"],"mappings":";KAAY;UAUK;WACN;WACA;WACA;;;cAeE,iCAAiC;WACnC,MAAM;WACN,SAAS,SAAS;EAEf,YAAA,MAAM,+BAA+B,SAAS;;;UAS3C;EACf,UAAU,qCAAqC,mBAAmB;EAClE;GACC,OAAO;;;;;iBCqEM,6BAA6B,wBAAwB"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{resolve as e}from"node:path";import{closeSync as t,constants as n,fstatSync as r,readSync as i}from"node:fs";import*as a from"koffi";const o={INVALID_PATH:`The requested root-relative file operation is invalid.`,UNSAFE_ENTRY:`The requested entry cannot be opened without following an unsafe object.`,UNSUPPORTED_BACKEND:`This host cannot provide stable root-relative file reads.`,AUTHORITY_CLOSED:`The stable rooted file authority is closed.`,ROOT_CHANGED:`The retained root identity changed during the operation.`,FILE_CHANGED:`The file identity or contents changed during the bounded read.`,OVER_BUDGET:`The file exceeds the supplied byte budget.`,HOST_IO:`The host could not complete the stable root-relative file operation.`};var s=class extends Error{code;context;constructor(e,t){super(o[e]),this.name=`StableFileAuthorityError`,this.code=e,this.context=Object.freeze({...t})}};function c(e,t,n={}){return new s(e,{operation:t,...n})}function l(e){return{device:e.dev,inode:e.ino,mode:e.mode}}function u(e){return{...l(e),size:e.size,modified:e.mtimeNs,changed:e.ctimeNs}}function d(e,t){return e.device===t.device&&e.inode===t.inode&&e.mode===t.mode}function f(e,t){return d(e,t)&&e.size===t.size&&e.modified===t.modified&&e.changed===t.changed}function p(e){for(let[t,n]of Object.entries(a.os.errno))if(n===e)return t;return`ERRNO_${e}`}function m(e){let t=n.O_CLOEXEC??(process.platform===`darwin`?16777216:524288);return n.O_RDONLY|n.O_NOFOLLOW|n.O_NONBLOCK|t|(e?n.O_DIRECTORY:0)}function h(e,t){try{return r(e,{bigint:!0})}catch(e){let n=g(typeof e==`object`?e:null);throw c(`HOST_IO`,t,n===void 0?{}:{hostCode:n})}}function g(e){if(!(e===null||!(`code`in e)))return typeof e.code==`string`?e.code:void 0}var _=class{openAt;rootIdentity;rootDescriptor;constructor(e){try{let t=a.load(process.platform===`darwin`?`/usr/lib/libSystem.B.dylib`:`libc.so.6`),n=t.func(`int open(const char *path, int flags)`);this.openAt=t.func(`int openat(int fd, const char *path, int flags)`);let r=n(e,m(!0));r<0&&this.throwOpenFailure(a.errno(),`open-root`),this.rootDescriptor=r;let i=h(r,`inspect-root`);if(!i.isDirectory())throw c(`UNSAFE_ENTRY`,`open-root`);this.rootIdentity=l(i)}catch(e){throw this.rootDescriptor!==void 0&&(this.closeDescriptor(this.rootDescriptor),this.rootDescriptor=void 0),e instanceof s?e:c(`UNSUPPORTED_BACKEND`,`load-posix-backend`)}}readBytes(e,t,n){let r=this.rootDescriptor;if(r===void 0)throw c(`AUTHORITY_CLOSED`,`read`);this.assertRootIdentity(r);let i=r,o=!1,l;try{for(let t=0;t<e.length;t+=1){let r=t===e.length-1,s=this.openAt(i,e[t],m(!r));if(s<0){let e=a.errno();if(e===a.os.errno.ENOENT)return;this.throwOpenFailure(e,r?`open-file`:`open-directory`,t)}if(r){l=s;break}if(!h(s,`inspect-directory`).isDirectory())throw this.closeDescriptor(s),c(`UNSAFE_ENTRY`,`inspect-directory`,{segmentIndex:t});o&&this.closeDescriptor(i),i=s,o=!0,n.afterDirectoryOpened?.(t)}if(l===void 0)throw c(`HOST_IO`,`open-file`);let s=this.readFile(l,t,n);return this.assertRootIdentity(r),s}catch(e){if(e instanceof s)throw e;let t=g(typeof e==`object`?e:null);throw c(`HOST_IO`,`read`,t===void 0?{}:{hostCode:t})}finally{this.closeOptionalDescriptor(l),this.closeOptionalDescriptor(o?i:void 0)}}close(){let e=this.rootDescriptor;e!==void 0&&(this.rootDescriptor=void 0,this.closeDescriptor(e))}readFile(e,t,n){let r=h(e,`inspect-file`);if(!r.isFile())throw c(`UNSAFE_ENTRY`,`inspect-file`);if(r.size>BigInt(t))throw c(`OVER_BUDGET`,`read-file`);let a=Number(r.size),o;try{o=Buffer.alloc(a)}catch{throw c(`HOST_IO`,`allocate-read-buffer`)}let s=0;for(;s<a;){let t=i(e,o,s,Math.min(1048576,a-s),s);if(t===0)break;s+=t,n.afterReadChunk?.(s)}let l=i(e,Buffer.alloc(1),0,1,a)!==0,d=h(e,`reinspect-file`);if(l&&a>=t)throw c(`OVER_BUDGET`,`read-file`);if(s!==a||l||!f(u(r),u(d)))throw c(`FILE_CHANGED`,`read-file`);return o}assertRootIdentity(e){let t=h(e,`inspect-root`);if(!t.isDirectory()||!d(this.rootIdentity,l(t)))throw c(`ROOT_CHANGED`,`inspect-root`)}throwOpenFailure(e,t,n){let r=p(e),i=n===void 0?{hostCode:r}:{hostCode:r,segmentIndex:n};throw e===a.os.errno.ELOOP||e===a.os.errno.ENOTDIR||e===a.os.errno.EISDIR?c(`UNSAFE_ENTRY`,t,i):c(`HOST_IO`,t,i)}closeDescriptor(e){try{t(e)}catch{}}closeOptionalDescriptor(e){e!==void 0&&this.closeDescriptor(e)}};function v(){let e=a.pointer(`RobotaStableFileHandle`,a.opaque()),t=a.struct(`RobotaUnicodeString`,{Length:`uint16_t`,MaximumLength:`uint16_t`,Buffer:a.pointer(`char16_t`)});return{handleType:e,unicodeStringType:t,objectAttributesType:a.struct(`RobotaObjectAttributes`,{Length:`uint32_t`,RootDirectory:e,ObjectName:a.pointer(t),Attributes:`uint32_t`,SecurityDescriptor:`void *`,SecurityQualityOfService:`void *`}),ioStatusBlockType:a.struct(`RobotaIoStatusBlock`,{Status:`intptr_t`,Information:`uintptr_t`}),attributeTagInfoType:a.struct(`RobotaFileAttributeTagInfo`,{FileAttributes:`uint32_t`,ReparseTag:`uint32_t`}),fileIdInfoType:a.struct(`RobotaFileIdInfo`,{VolumeSerialNumber:`uint64_t`,FileId:a.array(`uint8_t`,16,`Typed`)}),standardInfoType:a.struct(`RobotaFileStandardInfo`,{AllocationSize:`int64_t`,EndOfFile:`int64_t`,NumberOfLinks:`uint32_t`,DeletePending:`uint8_t`,Directory:`uint8_t`})}}function y(e,t){return{createFileW:e.func(`CreateFileW`,t.handleType,[a.pointer(`char16_t`),`uint32_t`,`uint32_t`,`void *`,`uint32_t`,`uint32_t`,t.handleType]),closeHandle:e.func(`CloseHandle`,`bool`,[t.handleType]),...b(e,t),getFileSizeEx:e.func(`GetFileSizeEx`,`bool`,[t.handleType,a.out(a.pointer(`int64_t`))]),readFile:e.func(`ReadFile`,`bool`,[t.handleType,a.out(a.pointer(`uint8_t`)),`uint32_t`,a.out(a.pointer(`uint32_t`)),`void *`]),getLastError:e.func(`GetLastError`,`uint32_t`,[])}}function b(e,t){let n=n=>e.func(`GetFileInformationByHandleEx`,`bool`,[t.handleType,`int32_t`,a.out(a.pointer(n)),`uint32_t`]);return{getAttributeTagInfo:n(t.attributeTagInfoType),getFileIdInfo:n(t.fileIdInfoType),getStandardInfo:n(t.standardInfoType)}}function x(e,t){return{ntCreateFile:e.func(`NtCreateFile`,`int32_t`,[a.out(a.pointer(t.handleType)),`uint32_t`,a.pointer(t.objectAttributesType),a.out(a.pointer(t.ioStatusBlockType)),`void *`,`uint32_t`,`uint32_t`,`uint32_t`,`uint32_t`,`void *`,`uint32_t`]),ntClose:e.func(`NtClose`,`int32_t`,[t.handleType])}}let S;function C(){if(S!==void 0)return S;let e=v();return S={...e,...y(a.load(`kernel32.dll`),e),...x(a.load(`ntdll.dll`),e)},S}const w=BigInt(`0`);function T(e){if(e instanceof Uint8Array||Array.isArray(e))return Buffer.from(e).toString(`hex`);throw c(`UNSUPPORTED_BACKEND`,`decode-file-identity`)}function E(e){return`WIN32_${e.getLastError()}`}function D(e,t,n){let r={};if(!e.getFileIdInfo(t,18,r,a.sizeof(e.fileIdInfoType)))throw c(`UNSUPPORTED_BACKEND`,n,{hostCode:E(e)});return{volume:BigInt(r.VolumeSerialNumber??w),fileId:T(r.FileId)}}function O(e,t,n,r){let i=A(e,t);if(i.size>BigInt(n))throw c(`OVER_BUDGET`,`read-file`);let a=Number(i.size),o;try{o=Buffer.alloc(a)}catch{throw c(`HOST_IO`,`allocate-read-buffer`)}let s=k(e,t,o,r),l=[0];if(!e.readFile(t,Buffer.alloc(1),1,l,null))throw c(`HOST_IO`,`probe-file`,{hostCode:E(e)});let u=A(e,t);if(l[0]!==0&&a>=n)throw c(`OVER_BUDGET`,`read-file`);if(!j(i,u,s,a,l[0]))throw c(`FILE_CHANGED`,`read-file`);return o}function k(e,t,n,r){let i=0;for(;i<n.length;){let a=Math.min(1048576,n.length-i),o=[0];if(!e.readFile(t,n.subarray(i,i+a),a,o,null))throw c(`HOST_IO`,`read-file`,{hostCode:E(e)});if(o[0]===0)break;i+=o[0],r.afterReadChunk?.(i)}return i}function A(e,t){let n=D(e,t,`inspect-file`),r=[w];if(!e.getFileSizeEx(t,r))throw c(`HOST_IO`,`inspect-file`,{hostCode:E(e)});return{...n,size:BigInt(r[0])}}function j(e,t,n,r,i){return n===r&&i===0&&e.volume===t.volume&&e.fileId===t.fileId&&e.size===t.size}const M=new Set([3221225487,3221225524,3221225530]),N=new Set([2147483693,3221225658,3221225731,3221226105,3221226763]);function P(e){let t=e>>>0;return{kind:M.has(t)?`missing`:N.has(t)?`unsafe`:`host-io`,hostCode:`NTSTATUS_0x${t.toString(16)}`}}const F=BigInt(`-1`);function I(e){return e===null||BigInt.asIntN(64,e)===F}var L=class{api;constructor(){try{this.api=C()}catch{throw c(`UNSUPPORTED_BACKEND`,`load-windows-backend`)}}openRoot(e){let t=this.api.createFileW(e,2147483648,7,null,3,35651584,null);if(I(t))throw c(`HOST_IO`,`open-root`,{hostCode:this.lastErrorCode()});try{return this.assertSafeHandle(t,!0,`inspect-root`),t}catch(e){throw this.close(t),e}}openRelative(e,t,n,r){let{handle:i,status:a}=this.issueRelativeOpen(e,t,n);if(a>=0){if(I(i))throw c(`HOST_IO`,`open-relative`);try{return this.assertSafeHandle(i,!n,n?`inspect-file`:`inspect-directory`,r),i}catch(e){throw this.close(i),e}}I(i)||this.close(i);let o=P(a);if(o.kind===`missing`)return;let s={segmentIndex:r,hostCode:o.hostCode};throw o.kind===`unsafe`?c(`UNSAFE_ENTRY`,n?`open-file`:`open-directory`,s):c(`HOST_IO`,n?`open-file`:`open-directory`,s)}readFile(e,t,n){return O(this.api,e,t,n)}queryIdentity(e,t){return D(this.api,e,t)}close(e){try{if(this.api.ntClose(e)>=0)return}catch{}try{this.api.closeHandle(e)}catch{}}issueRelativeOpen(e,t,n){let r=a.alloc(this.api.unicodeStringType,1);try{a.encode(r,this.api.unicodeStringType,{Length:Buffer.byteLength(t,`utf16le`),MaximumLength:Buffer.byteLength(`${t}\0`,`utf16le`),Buffer:t});let i={Length:a.sizeof(this.api.objectAttributesType),RootDirectory:e,ObjectName:r,Attributes:4160,SecurityDescriptor:null,SecurityQualityOfService:null},o=[null],s={Status:0,Information:0},c=n?5:7,l=this.api.ntCreateFile(o,1048705,i,s,null,0,c,1,(n?64:1)|2097184,null,0);return{handle:o[0],status:l}}finally{a.free(r)}}assertSafeHandle(e,t,n,r){let i={},o={};if(!this.api.getAttributeTagInfo(e,9,i,a.sizeof(this.api.attributeTagInfoType))||!this.api.getStandardInfo(e,1,o,a.sizeof(this.api.standardInfoType)))throw c(`HOST_IO`,n,{hostCode:this.lastErrorCode()});if((i.FileAttributes??0)&1024||!!o.Directory!==t)throw c(`UNSAFE_ENTRY`,n,r===void 0?{}:{segmentIndex:r})}lastErrorCode(){return E(this.api)}},R=class{native=new L;rootIdentity;rootHandle;constructor(e){try{let t=this.native.openRoot(e);this.rootHandle=t,this.rootIdentity=this.native.queryIdentity(t,`inspect-root`)}catch(e){throw this.rootHandle!==void 0&&(this.native.close(this.rootHandle),this.rootHandle=void 0),e instanceof s?e:c(`UNSUPPORTED_BACKEND`,`load-windows-backend`)}}readBytes(e,t,n){let r=this.rootHandle;if(r===void 0)throw c(`AUTHORITY_CLOSED`,`read`);this.assertRootIdentity(r);let i=r,a=!1,o;try{for(let t=0;t<e.length;t+=1){let r=t===e.length-1,s=this.native.openRelative(i,e[t],r,t);if(s===void 0)return;if(r){o=s;break}a&&this.native.close(i),i=s,a=!0,n.afterDirectoryOpened?.(t)}if(o===void 0)throw c(`HOST_IO`,`open-file`);let s=this.native.readFile(o,t,n);return this.assertRootIdentity(r),s}catch(e){throw e instanceof s?e:c(`HOST_IO`,`read`)}finally{o!==void 0&&this.native.close(o),a&&this.native.close(i)}}close(){let e=this.rootHandle;e!==void 0&&(this.rootHandle=void 0,this.native.close(e))}assertRootIdentity(e){let t=this.native.queryIdentity(e,`inspect-root`);if(t.volume!==this.rootIdentity.volume||t.fileId!==this.rootIdentity.fileId)throw c(`ROOT_CHANGED`,`inspect-root`)}};const z=new Set([`darwin-arm64`,`darwin-x64`,`linux-arm64`,`linux-x64`,`win32-x64`]),B=new FinalizationRegistry(e=>{e.close()});function V(t){if(typeof t!=`string`||t.trim().length===0||t.includes(`\0`))throw c(`INVALID_PATH`,`validate-root`);return e(t)}function H(e,t){if(!Array.isArray(e)||e.length===0)throw c(`INVALID_PATH`,`validate-segments`);for(let t=0;t<e.length;t+=1){let n=e[t];if(typeof n!=`string`||n.length===0||n===`.`||n===`..`||n.includes(`\0`)||n.includes(`/`)||n.includes(`\\`))throw c(`INVALID_PATH`,`validate-segments`,{segmentIndex:t})}if(!Number.isSafeInteger(t)||t<0)throw c(`INVALID_PATH`,`validate-budget`)}function U(e){let t=`${process.platform}-${process.arch}`;if(!z.has(t))throw c(`UNSUPPORTED_BACKEND`,`select-backend`,{hostCode:t});return process.platform===`win32`?new R(e):new _(e)}var W=class{hooks;backend;finalizerToken={};constructor(e,t){this.hooks=t;let n=U(V(e));this.backend=n,B.register(this,n,this.finalizerToken)}readBytes(e,t){let n=this.backend;if(n===void 0)throw c(`AUTHORITY_CLOSED`,`read`);H(e,t);try{return n.readBytes(e,t,this.hooks)}catch(e){throw e instanceof s?e:c(`HOST_IO`,`read`)}}close(){let e=this.backend;if(e!==void 0){this.backend=void 0,B.unregister(this.finalizerToken);try{e.close()}catch(e){throw e instanceof s?e:c(`HOST_IO`,`close`)}}}[Symbol.dispose](){this.close()}};function G(e){return new W(e,{})}export{s as StableFileAuthorityError,G as createStableRootedFileReader};
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["CHUNK_BYTES"],"sources":["../../src/contracts.ts","../../src/posix-backend.ts","../../src/windows-native-api.ts","../../src/windows-native-read.ts","../../src/windows-native-status.ts","../../src/windows-native-authority.ts","../../src/windows-backend.ts","../../src/stable-rooted-file-reader.ts"],"sourcesContent":["export type TStableFileAuthorityErrorCode =\n | 'INVALID_PATH'\n | 'UNSAFE_ENTRY'\n | 'UNSUPPORTED_BACKEND'\n | 'AUTHORITY_CLOSED'\n | 'ROOT_CHANGED'\n | 'FILE_CHANGED'\n | 'OVER_BUDGET'\n | 'HOST_IO';\n\nexport interface IStableFileAuthorityErrorContext {\n readonly operation: string;\n readonly segmentIndex?: number;\n readonly hostCode?: string;\n}\n\nconst ERROR_MESSAGES: Readonly<Record<TStableFileAuthorityErrorCode, string>> = {\n INVALID_PATH: 'The requested root-relative file operation is invalid.',\n UNSAFE_ENTRY: 'The requested entry cannot be opened without following an unsafe object.',\n UNSUPPORTED_BACKEND: 'This host cannot provide stable root-relative file reads.',\n AUTHORITY_CLOSED: 'The stable rooted file authority is closed.',\n ROOT_CHANGED: 'The retained root identity changed during the operation.',\n FILE_CHANGED: 'The file identity or contents changed during the bounded read.',\n OVER_BUDGET: 'The file exceeds the supplied byte budget.',\n HOST_IO: 'The host could not complete the stable root-relative file operation.',\n};\n\n/** A stable, secret-free failure from a rooted file authority. */\nexport class StableFileAuthorityError extends Error {\n readonly code: TStableFileAuthorityErrorCode;\n readonly context: Readonly<IStableFileAuthorityErrorContext>;\n\n constructor(code: TStableFileAuthorityErrorCode, context: IStableFileAuthorityErrorContext) {\n super(ERROR_MESSAGES[code]);\n this.name = 'StableFileAuthorityError';\n this.code = code;\n this.context = Object.freeze({ ...context });\n }\n}\n\n/** Opaque authority for bounded regular-file reads beneath one retained directory identity. */\nexport interface IStableRootedFileReader {\n readBytes(relativeSegments: readonly string[], maxBytes: number): Uint8Array | undefined;\n close(): void;\n [Symbol.dispose](): void;\n}\n\nexport function authorityError(\n code: TStableFileAuthorityErrorCode,\n operation: string,\n details: Omit<IStableFileAuthorityErrorContext, 'operation'> = {},\n): StableFileAuthorityError {\n return new StableFileAuthorityError(code, { operation, ...details });\n}\n","import { closeSync, constants, fstatSync, readSync, type BigIntStats } from 'node:fs';\n\nimport * as koffi from 'koffi';\n\nimport { authorityError, StableFileAuthorityError } from './contracts.js';\n\nimport type { IFileAuthorityTestHooks, IStableFileHostBackend } from './host-backend.js';\n\ninterface IPosixIdentity {\n readonly device: bigint;\n readonly inode: bigint;\n readonly mode: bigint;\n}\n\ninterface IPosixFileSnapshot extends IPosixIdentity {\n readonly size: bigint;\n readonly modified: bigint;\n readonly changed: bigint;\n}\n\ntype TOpen = (path: string, flags: number) => number;\ntype TOpenAt = (directory: number, path: string, flags: number) => number;\n\nconst CHUNK_BYTES = 1_048_576;\nconst DARWIN_CLOSE_ON_EXEC = 0x01000000;\nconst LINUX_CLOSE_ON_EXEC = 0x00080000;\n\nfunction identity(metadata: BigIntStats): IPosixIdentity {\n return { device: metadata.dev, inode: metadata.ino, mode: metadata.mode };\n}\n\nfunction snapshot(metadata: BigIntStats): IPosixFileSnapshot {\n return {\n ...identity(metadata),\n size: metadata.size,\n modified: metadata.mtimeNs,\n changed: metadata.ctimeNs,\n };\n}\n\nfunction sameIdentity(left: IPosixIdentity, right: IPosixIdentity): boolean {\n return left.device === right.device && left.inode === right.inode && left.mode === right.mode;\n}\n\nfunction sameSnapshot(left: IPosixFileSnapshot, right: IPosixFileSnapshot): boolean {\n return (\n sameIdentity(left, right) &&\n left.size === right.size &&\n left.modified === right.modified &&\n left.changed === right.changed\n );\n}\n\nfunction hostCode(errno: number): string {\n for (const [name, value] of Object.entries(koffi.os.errno)) {\n if (value === errno) return name;\n }\n return `ERRNO_${errno}`;\n}\n\nfunction flags(directory: boolean): number {\n const closeOnExec =\n (constants as typeof constants & { readonly O_CLOEXEC?: number }).O_CLOEXEC ??\n (process.platform === 'darwin' ? DARWIN_CLOSE_ON_EXEC : LINUX_CLOSE_ON_EXEC);\n return (\n constants.O_RDONLY |\n constants.O_NOFOLLOW |\n constants.O_NONBLOCK |\n closeOnExec |\n (directory ? constants.O_DIRECTORY : 0)\n );\n}\n\nfunction safeFstat(descriptor: number, operation: string): BigIntStats {\n try {\n return fstatSync(descriptor, { bigint: true });\n } catch (error) {\n const code = nodeErrorCode(typeof error === 'object' ? error : null);\n throw authorityError('HOST_IO', operation, code === undefined ? {} : { hostCode: code });\n }\n}\n\nfunction nodeErrorCode(error: object | null): string | undefined {\n if (error === null || !('code' in error)) return undefined;\n return typeof error.code === 'string' ? error.code : undefined;\n}\n\n/** POSIX implementation; descriptors never leave this module. */\nexport class PosixStableFileHostBackend implements IStableFileHostBackend {\n private readonly openAt: TOpenAt;\n private readonly rootIdentity: IPosixIdentity;\n private rootDescriptor: number | undefined;\n\n constructor(rootDirectory: string) {\n try {\n const libc = koffi.load(\n process.platform === 'darwin' ? '/usr/lib/libSystem.B.dylib' : 'libc.so.6',\n );\n const open = libc.func('int open(const char *path, int flags)') as TOpen;\n this.openAt = libc.func('int openat(int fd, const char *path, int flags)') as TOpenAt;\n const descriptor = open(rootDirectory, flags(true));\n if (descriptor < 0) this.throwOpenFailure(koffi.errno(), 'open-root');\n this.rootDescriptor = descriptor;\n const metadata = safeFstat(descriptor, 'inspect-root');\n if (!metadata.isDirectory()) throw authorityError('UNSAFE_ENTRY', 'open-root');\n this.rootIdentity = identity(metadata);\n } catch (error) {\n if (this.rootDescriptor !== undefined) {\n this.closeDescriptor(this.rootDescriptor);\n this.rootDescriptor = undefined;\n }\n if (error instanceof StableFileAuthorityError) throw error;\n throw authorityError('UNSUPPORTED_BACKEND', 'load-posix-backend');\n }\n }\n\n readBytes(\n relativeSegments: readonly string[],\n maxBytes: number,\n hooks: IFileAuthorityTestHooks,\n ): Uint8Array | undefined {\n const rootDescriptor = this.rootDescriptor;\n if (rootDescriptor === undefined) throw authorityError('AUTHORITY_CLOSED', 'read');\n this.assertRootIdentity(rootDescriptor);\n\n let parentDescriptor = rootDescriptor;\n let ownsParent = false;\n let fileDescriptor: number | undefined;\n try {\n for (let index = 0; index < relativeSegments.length; index += 1) {\n const final = index === relativeSegments.length - 1;\n const descriptor = this.openAt(parentDescriptor, relativeSegments[index], flags(!final));\n if (descriptor < 0) {\n const errno = koffi.errno();\n if (errno === koffi.os.errno.ENOENT) return undefined;\n this.throwOpenFailure(errno, final ? 'open-file' : 'open-directory', index);\n }\n\n if (final) {\n fileDescriptor = descriptor;\n break;\n }\n\n const metadata = safeFstat(descriptor, 'inspect-directory');\n if (!metadata.isDirectory()) {\n this.closeDescriptor(descriptor);\n throw authorityError('UNSAFE_ENTRY', 'inspect-directory', { segmentIndex: index });\n }\n if (ownsParent) this.closeDescriptor(parentDescriptor);\n parentDescriptor = descriptor;\n ownsParent = true;\n hooks.afterDirectoryOpened?.(index);\n }\n\n if (fileDescriptor === undefined) throw authorityError('HOST_IO', 'open-file');\n const bytes = this.readFile(fileDescriptor, maxBytes, hooks);\n this.assertRootIdentity(rootDescriptor);\n return bytes;\n } catch (error) {\n if (error instanceof StableFileAuthorityError) throw error;\n const code = nodeErrorCode(typeof error === 'object' ? error : null);\n throw authorityError('HOST_IO', 'read', code === undefined ? {} : { hostCode: code });\n } finally {\n this.closeOptionalDescriptor(fileDescriptor);\n this.closeOptionalDescriptor(ownsParent ? parentDescriptor : undefined);\n }\n }\n\n close(): void {\n const descriptor = this.rootDescriptor;\n if (descriptor === undefined) return;\n this.rootDescriptor = undefined;\n this.closeDescriptor(descriptor);\n }\n\n private readFile(\n descriptor: number,\n maxBytes: number,\n hooks: IFileAuthorityTestHooks,\n ): Uint8Array {\n const beforeMetadata = safeFstat(descriptor, 'inspect-file');\n if (!beforeMetadata.isFile()) throw authorityError('UNSAFE_ENTRY', 'inspect-file');\n if (beforeMetadata.size > BigInt(maxBytes)) throw authorityError('OVER_BUDGET', 'read-file');\n\n const expectedBytes = Number(beforeMetadata.size);\n let bytes: Buffer;\n try {\n bytes = Buffer.alloc(expectedBytes);\n } catch {\n throw authorityError('HOST_IO', 'allocate-read-buffer');\n }\n\n let offset = 0;\n while (offset < expectedBytes) {\n const count = readSync(\n descriptor,\n bytes,\n offset,\n Math.min(CHUNK_BYTES, expectedBytes - offset),\n offset,\n );\n if (count === 0) break;\n offset += count;\n hooks.afterReadChunk?.(offset);\n }\n\n const probe = Buffer.alloc(1);\n const grew = readSync(descriptor, probe, 0, 1, expectedBytes) !== 0;\n const afterMetadata = safeFstat(descriptor, 'reinspect-file');\n if (grew && expectedBytes >= maxBytes) throw authorityError('OVER_BUDGET', 'read-file');\n if (\n offset !== expectedBytes ||\n grew ||\n !sameSnapshot(snapshot(beforeMetadata), snapshot(afterMetadata))\n ) {\n throw authorityError('FILE_CHANGED', 'read-file');\n }\n return bytes;\n }\n\n private assertRootIdentity(descriptor: number): void {\n const metadata = safeFstat(descriptor, 'inspect-root');\n if (!metadata.isDirectory() || !sameIdentity(this.rootIdentity, identity(metadata))) {\n throw authorityError('ROOT_CHANGED', 'inspect-root');\n }\n }\n\n private throwOpenFailure(errno: number, operation: string, segmentIndex?: number): never {\n const code = hostCode(errno);\n const details =\n segmentIndex === undefined ? { hostCode: code } : { hostCode: code, segmentIndex };\n if (\n errno === koffi.os.errno.ELOOP ||\n errno === koffi.os.errno.ENOTDIR ||\n errno === koffi.os.errno.EISDIR\n ) {\n throw authorityError('UNSAFE_ENTRY', operation, details);\n }\n throw authorityError('HOST_IO', operation, details);\n }\n\n private closeDescriptor(descriptor: number): void {\n try {\n closeSync(descriptor);\n } catch {\n // allow-fallback: descriptor close failures cannot restore authority or make a later read safe\n }\n }\n\n private closeOptionalDescriptor(descriptor: number | undefined): void {\n if (descriptor !== undefined) this.closeDescriptor(descriptor);\n }\n}\n","import * as koffi from 'koffi';\n\nexport type TNativeHandle = bigint | null;\n\nexport interface IWindowsAttributeTagInfo {\n FileAttributes?: number;\n ReparseTag?: number;\n}\n\nexport interface IWindowsFileIdInfo {\n VolumeSerialNumber?: bigint | number;\n FileId?: Uint8Array | number[];\n}\n\nexport interface IWindowsStandardInfo {\n Directory?: number;\n}\n\nexport interface IWindowsObjectAttributes {\n Length: number;\n RootDirectory: TNativeHandle;\n ObjectName: object;\n Attributes: number;\n SecurityDescriptor: null;\n SecurityQualityOfService: null;\n}\n\nexport interface IWindowsIoStatus {\n Status: number;\n Information: number;\n}\n\ntype TCreateFileW = (\n path: string,\n desiredAccess: number,\n shareMode: number,\n securityAttributes: null,\n creationDisposition: number,\n flagsAndAttributes: number,\n templateFile: TNativeHandle,\n) => TNativeHandle;\n\ntype TGetInformation = (\n handle: TNativeHandle,\n informationClass: number,\n output: object,\n outputBytes: number,\n) => boolean;\n\ntype TNtCreateFile = (\n output: TNativeHandle[],\n desiredAccess: number,\n objectAttributes: IWindowsObjectAttributes,\n ioStatus: IWindowsIoStatus,\n allocationSize: null,\n fileAttributes: number,\n shareAccess: number,\n createDisposition: number,\n createOptions: number,\n extendedAttributes: null,\n extendedAttributesBytes: number,\n) => number;\n\nexport interface IWindowsApi {\n readonly handleType: koffi.TypeObject;\n readonly unicodeStringType: koffi.TypeObject;\n readonly objectAttributesType: koffi.TypeObject;\n readonly attributeTagInfoType: koffi.TypeObject;\n readonly fileIdInfoType: koffi.TypeObject;\n readonly standardInfoType: koffi.TypeObject;\n readonly createFileW: TCreateFileW;\n readonly closeHandle: (handle: TNativeHandle) => boolean;\n readonly getAttributeTagInfo: TGetInformation;\n readonly getFileIdInfo: TGetInformation;\n readonly getStandardInfo: TGetInformation;\n readonly getFileSizeEx: (handle: TNativeHandle, size: bigint[]) => boolean;\n readonly readFile: (\n handle: TNativeHandle,\n buffer: Uint8Array,\n bytesToRead: number,\n bytesRead: number[],\n overlapped: null,\n ) => boolean;\n readonly getLastError: () => number;\n readonly ntCreateFile: TNtCreateFile;\n readonly ntClose: (handle: TNativeHandle) => number;\n}\n\ninterface IWindowsTypes {\n readonly handleType: koffi.TypeObject;\n readonly unicodeStringType: koffi.TypeObject;\n readonly objectAttributesType: koffi.TypeObject;\n readonly ioStatusBlockType: koffi.TypeObject;\n readonly attributeTagInfoType: koffi.TypeObject;\n readonly fileIdInfoType: koffi.TypeObject;\n readonly standardInfoType: koffi.TypeObject;\n}\n\nconst FILE_ID_BYTES = 16;\n\nfunction defineWindowsTypes(): IWindowsTypes {\n const handleType = koffi.pointer('RobotaStableFileHandle', koffi.opaque());\n const unicodeStringType = koffi.struct('RobotaUnicodeString', {\n Length: 'uint16_t',\n MaximumLength: 'uint16_t',\n Buffer: koffi.pointer('char16_t'),\n });\n const objectAttributesType = koffi.struct('RobotaObjectAttributes', {\n Length: 'uint32_t',\n RootDirectory: handleType,\n ObjectName: koffi.pointer(unicodeStringType),\n Attributes: 'uint32_t',\n SecurityDescriptor: 'void *',\n SecurityQualityOfService: 'void *',\n });\n return {\n handleType,\n unicodeStringType,\n objectAttributesType,\n ioStatusBlockType: koffi.struct('RobotaIoStatusBlock', {\n Status: 'intptr_t',\n Information: 'uintptr_t',\n }),\n attributeTagInfoType: koffi.struct('RobotaFileAttributeTagInfo', {\n FileAttributes: 'uint32_t',\n ReparseTag: 'uint32_t',\n }),\n fileIdInfoType: koffi.struct('RobotaFileIdInfo', {\n VolumeSerialNumber: 'uint64_t',\n FileId: koffi.array('uint8_t', FILE_ID_BYTES, 'Typed'),\n }),\n standardInfoType: koffi.struct('RobotaFileStandardInfo', {\n AllocationSize: 'int64_t',\n EndOfFile: 'int64_t',\n NumberOfLinks: 'uint32_t',\n DeletePending: 'uint8_t',\n Directory: 'uint8_t',\n }),\n };\n}\n\nfunction bindKernel32(\n library: koffi.LibraryHandle,\n types: IWindowsTypes,\n): Pick<\n IWindowsApi,\n | 'createFileW'\n | 'closeHandle'\n | 'getAttributeTagInfo'\n | 'getFileIdInfo'\n | 'getStandardInfo'\n | 'getFileSizeEx'\n | 'readFile'\n | 'getLastError'\n> {\n return {\n createFileW: library.func('CreateFileW', types.handleType, [\n koffi.pointer('char16_t'),\n 'uint32_t',\n 'uint32_t',\n 'void *',\n 'uint32_t',\n 'uint32_t',\n types.handleType,\n ]) as TCreateFileW,\n closeHandle: library.func('CloseHandle', 'bool', [types.handleType]),\n ...bindInformationFunctions(library, types),\n getFileSizeEx: library.func('GetFileSizeEx', 'bool', [\n types.handleType,\n koffi.out(koffi.pointer('int64_t')),\n ]),\n readFile: library.func('ReadFile', 'bool', [\n types.handleType,\n koffi.out(koffi.pointer('uint8_t')),\n 'uint32_t',\n koffi.out(koffi.pointer('uint32_t')),\n 'void *',\n ]),\n getLastError: library.func('GetLastError', 'uint32_t', []),\n };\n}\n\nfunction bindInformationFunctions(\n library: koffi.LibraryHandle,\n types: IWindowsTypes,\n): Pick<IWindowsApi, 'getAttributeTagInfo' | 'getFileIdInfo' | 'getStandardInfo'> {\n const bind = (outputType: koffi.TypeObject): TGetInformation =>\n library.func('GetFileInformationByHandleEx', 'bool', [\n types.handleType,\n 'int32_t',\n koffi.out(koffi.pointer(outputType)),\n 'uint32_t',\n ]) as TGetInformation;\n return {\n getAttributeTagInfo: bind(types.attributeTagInfoType),\n getFileIdInfo: bind(types.fileIdInfoType),\n getStandardInfo: bind(types.standardInfoType),\n };\n}\n\nfunction bindNtDll(\n library: koffi.LibraryHandle,\n types: IWindowsTypes,\n): Pick<IWindowsApi, 'ntCreateFile' | 'ntClose'> {\n return {\n ntCreateFile: library.func('NtCreateFile', 'int32_t', [\n koffi.out(koffi.pointer(types.handleType)),\n 'uint32_t',\n koffi.pointer(types.objectAttributesType),\n koffi.out(koffi.pointer(types.ioStatusBlockType)),\n 'void *',\n 'uint32_t',\n 'uint32_t',\n 'uint32_t',\n 'uint32_t',\n 'void *',\n 'uint32_t',\n ]) as TNtCreateFile,\n ntClose: library.func('NtClose', 'int32_t', [types.handleType]),\n };\n}\n\nlet cachedApi: IWindowsApi | undefined;\n\nexport function getWindowsApi(): IWindowsApi {\n if (cachedApi !== undefined) return cachedApi;\n const types = defineWindowsTypes();\n cachedApi = {\n ...types,\n ...bindKernel32(koffi.load('kernel32.dll'), types),\n ...bindNtDll(koffi.load('ntdll.dll'), types),\n };\n return cachedApi;\n}\n","import * as koffi from 'koffi';\n\nimport { authorityError } from './contracts.js';\n\nimport type { IFileAuthorityTestHooks } from './host-backend.js';\nimport type { IWindowsApi, IWindowsFileIdInfo, TNativeHandle } from './windows-native-api.js';\n\nexport interface IWindowsIdentity {\n readonly volume: bigint;\n readonly fileId: string;\n}\n\ninterface IWindowsFileSnapshot extends IWindowsIdentity {\n readonly size: bigint;\n}\n\nconst FILE_ID_INFO_CLASS = 18;\nconst CHUNK_BYTES = 1_048_576;\nconst ZERO_SIZE = BigInt('0');\n\nfunction bytesToHex(value: Uint8Array | number[] | undefined): string {\n if (value instanceof Uint8Array) return Buffer.from(value).toString('hex');\n if (Array.isArray(value)) return Buffer.from(value).toString('hex');\n throw authorityError('UNSUPPORTED_BACKEND', 'decode-file-identity');\n}\n\nexport function windowsLastErrorCode(api: IWindowsApi): string {\n return `WIN32_${api.getLastError()}`;\n}\n\nexport function queryWindowsIdentity(\n api: IWindowsApi,\n handle: TNativeHandle,\n operation: string,\n): IWindowsIdentity {\n const info: IWindowsFileIdInfo = {};\n if (!api.getFileIdInfo(handle, FILE_ID_INFO_CLASS, info, koffi.sizeof(api.fileIdInfoType))) {\n throw authorityError('UNSUPPORTED_BACKEND', operation, {\n hostCode: windowsLastErrorCode(api),\n });\n }\n return {\n volume: BigInt(info.VolumeSerialNumber ?? ZERO_SIZE),\n fileId: bytesToHex(info.FileId),\n };\n}\n\nexport function readWindowsFile(\n api: IWindowsApi,\n handle: TNativeHandle,\n maxBytes: number,\n hooks: IFileAuthorityTestHooks,\n): Uint8Array {\n const before = queryFileSnapshot(api, handle);\n if (before.size > BigInt(maxBytes)) throw authorityError('OVER_BUDGET', 'read-file');\n const expectedBytes = Number(before.size);\n let bytes: Buffer;\n try {\n bytes = Buffer.alloc(expectedBytes);\n } catch {\n throw authorityError('HOST_IO', 'allocate-read-buffer');\n }\n\n const offset = readExpectedBytes(api, handle, bytes, hooks);\n const probeCount = [0];\n if (!api.readFile(handle, Buffer.alloc(1), 1, probeCount, null)) {\n throw authorityError('HOST_IO', 'probe-file', { hostCode: windowsLastErrorCode(api) });\n }\n const after = queryFileSnapshot(api, handle);\n if (probeCount[0] !== 0 && expectedBytes >= maxBytes) {\n throw authorityError('OVER_BUDGET', 'read-file');\n }\n if (!isStableRead(before, after, offset, expectedBytes, probeCount[0])) {\n throw authorityError('FILE_CHANGED', 'read-file');\n }\n return bytes;\n}\n\nfunction readExpectedBytes(\n api: IWindowsApi,\n handle: TNativeHandle,\n bytes: Buffer,\n hooks: IFileAuthorityTestHooks,\n): number {\n let offset = 0;\n while (offset < bytes.length) {\n const length = Math.min(CHUNK_BYTES, bytes.length - offset);\n const count = [0];\n if (!api.readFile(handle, bytes.subarray(offset, offset + length), length, count, null)) {\n throw authorityError('HOST_IO', 'read-file', { hostCode: windowsLastErrorCode(api) });\n }\n if (count[0] === 0) break;\n offset += count[0];\n hooks.afterReadChunk?.(offset);\n }\n return offset;\n}\n\nfunction queryFileSnapshot(api: IWindowsApi, handle: TNativeHandle): IWindowsFileSnapshot {\n const identity = queryWindowsIdentity(api, handle, 'inspect-file');\n const size = [ZERO_SIZE];\n if (!api.getFileSizeEx(handle, size)) {\n throw authorityError('HOST_IO', 'inspect-file', { hostCode: windowsLastErrorCode(api) });\n }\n return { ...identity, size: BigInt(size[0]) };\n}\n\nfunction isStableRead(\n before: IWindowsFileSnapshot,\n after: IWindowsFileSnapshot,\n actualBytes: number,\n expectedBytes: number,\n probeBytes: number,\n): boolean {\n return (\n actualBytes === expectedBytes &&\n probeBytes === 0 &&\n before.volume === after.volume &&\n before.fileId === after.fileId &&\n before.size === after.size\n );\n}\n","type TNtOpenFailureKind = 'missing' | 'unsafe' | 'host-io';\n\nexport interface INtOpenFailure {\n readonly kind: TNtOpenFailureKind;\n readonly hostCode: string;\n}\n\nconst HEX_RADIX = 16;\nconst STATUS_NO_SUCH_FILE = 0xc000000f;\nconst STATUS_OBJECT_NAME_NOT_FOUND = 0xc0000034;\nconst STATUS_OBJECT_PATH_NOT_FOUND = 0xc000003a;\nconst STATUS_STOPPED_ON_SYMLINK = 0x8000002d;\nconst STATUS_FILE_IS_A_DIRECTORY = 0xc00000ba;\nconst STATUS_NOT_A_DIRECTORY = 0xc0000103;\nconst STATUS_REPARSE_POINT_ENCOUNTERED = 0xc0000279;\nconst STATUS_IO_REPARSE_TAG_NOT_HANDLED = 0xc000050b;\n\nconst MISSING_STATUSES = new Set([\n STATUS_NO_SUCH_FILE,\n STATUS_OBJECT_NAME_NOT_FOUND,\n STATUS_OBJECT_PATH_NOT_FOUND,\n]);\nconst UNSAFE_STATUSES = new Set([\n STATUS_STOPPED_ON_SYMLINK,\n STATUS_FILE_IS_A_DIRECTORY,\n STATUS_NOT_A_DIRECTORY,\n STATUS_REPARSE_POINT_ENCOUNTERED,\n STATUS_IO_REPARSE_TAG_NOT_HANDLED,\n]);\n\nexport function classifyNtOpenFailure(status: number): INtOpenFailure {\n const normalized = status >>> 0;\n return {\n kind: MISSING_STATUSES.has(normalized)\n ? 'missing'\n : UNSAFE_STATUSES.has(normalized)\n ? 'unsafe'\n : 'host-io',\n hostCode: `NTSTATUS_0x${normalized.toString(HEX_RADIX)}`,\n };\n}\n","import * as koffi from 'koffi';\n\nimport { authorityError } from './contracts.js';\nimport {\n getWindowsApi,\n type IWindowsApi,\n type IWindowsAttributeTagInfo,\n type IWindowsIoStatus,\n type IWindowsObjectAttributes,\n type IWindowsStandardInfo,\n type TNativeHandle,\n} from './windows-native-api.js';\nimport {\n queryWindowsIdentity,\n readWindowsFile,\n windowsLastErrorCode,\n type IWindowsIdentity,\n} from './windows-native-read.js';\nimport { classifyNtOpenFailure } from './windows-native-status.js';\n\nimport type { IFileAuthorityTestHooks } from './host-backend.js';\n\nexport type { IWindowsIdentity } from './windows-native-read.js';\n\nconst GENERIC_READ = 0x80000000;\nconst FILE_READ_DATA = 0x0001;\nconst FILE_LIST_DIRECTORY = 0x0001;\nconst FILE_READ_ATTRIBUTES = 0x0080;\nconst SYNCHRONIZE = 0x00100000;\nconst FILE_SHARE_READ = 0x00000001;\nconst FILE_SHARE_WRITE = 0x00000002;\nconst FILE_SHARE_DELETE = 0x00000004;\nconst OPEN_EXISTING = 3;\nconst FILE_ATTRIBUTE_REPARSE_POINT = 0x00000400;\nconst FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000;\nconst FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;\nconst FILE_OPEN = 1;\nconst FILE_DIRECTORY_FILE = 0x00000001;\nconst FILE_SYNCHRONOUS_IO_NONALERT = 0x00000020;\nconst FILE_NON_DIRECTORY_FILE = 0x00000040;\nconst FILE_OPEN_REPARSE_POINT = 0x00200000;\nconst OBJ_CASE_INSENSITIVE = 0x00000040;\nconst OBJ_DONT_REPARSE = 0x00001000;\nconst FILE_STANDARD_INFO_CLASS = 1;\nconst FILE_ATTRIBUTE_TAG_INFO_CLASS = 9;\nconst HANDLE_WIDTH_BITS = 64;\nconst INVALID_HANDLE_VALUE = BigInt('-1');\n\nfunction isInvalidHandle(handle: TNativeHandle): boolean {\n return handle === null || BigInt.asIntN(HANDLE_WIDTH_BITS, handle) === INVALID_HANDLE_VALUE;\n}\n\n/** Raw Windows handle operations. Native HANDLE and Koffi values stay inside this module. */\nexport class WindowsNativeAuthority {\n private readonly api: IWindowsApi;\n\n constructor() {\n try {\n this.api = getWindowsApi();\n } catch {\n throw authorityError('UNSUPPORTED_BACKEND', 'load-windows-backend');\n }\n }\n\n openRoot(rootDirectory: string): TNativeHandle {\n const handle = this.api.createFileW(\n rootDirectory,\n GENERIC_READ,\n FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,\n null,\n OPEN_EXISTING,\n FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,\n null,\n );\n if (isInvalidHandle(handle)) {\n throw authorityError('HOST_IO', 'open-root', { hostCode: this.lastErrorCode() });\n }\n try {\n this.assertSafeHandle(handle, true, 'inspect-root');\n return handle;\n } catch (error) {\n this.close(handle);\n throw error;\n }\n }\n\n openRelative(\n parentHandle: TNativeHandle,\n segment: string,\n final: boolean,\n segmentIndex: number,\n ): TNativeHandle | undefined {\n const { handle, status } = this.issueRelativeOpen(parentHandle, segment, final);\n if (status >= 0) {\n if (isInvalidHandle(handle)) throw authorityError('HOST_IO', 'open-relative');\n try {\n this.assertSafeHandle(\n handle,\n !final,\n final ? 'inspect-file' : 'inspect-directory',\n segmentIndex,\n );\n return handle;\n } catch (error) {\n this.close(handle);\n throw error;\n }\n }\n if (!isInvalidHandle(handle)) this.close(handle);\n const failure = classifyNtOpenFailure(status);\n if (failure.kind === 'missing') return undefined;\n const details = { segmentIndex, hostCode: failure.hostCode };\n if (failure.kind === 'unsafe') {\n throw authorityError('UNSAFE_ENTRY', final ? 'open-file' : 'open-directory', details);\n }\n throw authorityError('HOST_IO', final ? 'open-file' : 'open-directory', details);\n }\n\n readFile(handle: TNativeHandle, maxBytes: number, hooks: IFileAuthorityTestHooks): Uint8Array {\n return readWindowsFile(this.api, handle, maxBytes, hooks);\n }\n\n queryIdentity(handle: TNativeHandle, operation: string): IWindowsIdentity {\n return queryWindowsIdentity(this.api, handle, operation);\n }\n\n close(handle: TNativeHandle): void {\n try {\n if (this.api.ntClose(handle) >= 0) return;\n } catch {\n // allow-fallback: CloseHandle is the native cleanup fallback after NtClose itself throws\n }\n try {\n this.api.closeHandle(handle);\n } catch {\n // allow-fallback: close failure cannot restore authority or make a later read safe\n }\n }\n\n private issueRelativeOpen(\n parentHandle: TNativeHandle,\n segment: string,\n final: boolean,\n ): { handle: TNativeHandle; status: number } {\n const unicodeName = koffi.alloc(this.api.unicodeStringType, 1);\n try {\n koffi.encode(unicodeName, this.api.unicodeStringType, {\n Length: Buffer.byteLength(segment, 'utf16le'),\n MaximumLength: Buffer.byteLength(`${segment}\\0`, 'utf16le'),\n Buffer: segment,\n });\n const attributes: IWindowsObjectAttributes = {\n Length: koffi.sizeof(this.api.objectAttributesType),\n RootDirectory: parentHandle,\n ObjectName: unicodeName,\n Attributes: OBJ_CASE_INSENSITIVE | OBJ_DONT_REPARSE,\n SecurityDescriptor: null,\n SecurityQualityOfService: null,\n };\n const output: TNativeHandle[] = [null];\n const ioStatus: IWindowsIoStatus = { Status: 0, Information: 0 };\n // A final read cannot verify same-size in-place rewrites from identity and size alone.\n // Deny write sharing for that handle so existing and new writers cannot race the read.\n const shareAccess = final\n ? FILE_SHARE_READ | FILE_SHARE_DELETE\n : FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;\n const status = this.api.ntCreateFile(\n output,\n (final ? FILE_READ_DATA : FILE_LIST_DIRECTORY) | FILE_READ_ATTRIBUTES | SYNCHRONIZE,\n attributes,\n ioStatus,\n null,\n 0,\n shareAccess,\n FILE_OPEN,\n (final ? FILE_NON_DIRECTORY_FILE : FILE_DIRECTORY_FILE) |\n FILE_SYNCHRONOUS_IO_NONALERT |\n FILE_OPEN_REPARSE_POINT,\n null,\n 0,\n );\n return { handle: output[0], status };\n } finally {\n koffi.free(unicodeName);\n }\n }\n\n private assertSafeHandle(\n handle: TNativeHandle,\n directory: boolean,\n operation: string,\n segmentIndex?: number,\n ): void {\n const attributes: IWindowsAttributeTagInfo = {};\n const standard: IWindowsStandardInfo = {};\n if (\n !this.api.getAttributeTagInfo(\n handle,\n FILE_ATTRIBUTE_TAG_INFO_CLASS,\n attributes,\n koffi.sizeof(this.api.attributeTagInfoType),\n ) ||\n !this.api.getStandardInfo(\n handle,\n FILE_STANDARD_INFO_CLASS,\n standard,\n koffi.sizeof(this.api.standardInfoType),\n )\n ) {\n throw authorityError('HOST_IO', operation, { hostCode: this.lastErrorCode() });\n }\n const unsafe =\n ((attributes.FileAttributes ?? 0) & FILE_ATTRIBUTE_REPARSE_POINT) !== 0 ||\n Boolean(standard.Directory) !== directory;\n if (unsafe) {\n throw authorityError(\n 'UNSAFE_ENTRY',\n operation,\n segmentIndex === undefined ? {} : { segmentIndex },\n );\n }\n }\n\n private lastErrorCode(): string {\n return windowsLastErrorCode(this.api);\n }\n}\n","import { authorityError, StableFileAuthorityError } from './contracts.js';\nimport { WindowsNativeAuthority, type IWindowsIdentity } from './windows-native-authority.js';\n\nimport type { IFileAuthorityTestHooks, IStableFileHostBackend } from './host-backend.js';\nimport type { TNativeHandle } from './windows-native-api.js';\n\n/** Windows implementation; native HANDLE values never leave the native authority module. */\nexport class WindowsStableFileHostBackend implements IStableFileHostBackend {\n private readonly native = new WindowsNativeAuthority();\n private readonly rootIdentity: IWindowsIdentity;\n private rootHandle: TNativeHandle | undefined;\n\n constructor(rootDirectory: string) {\n try {\n const handle = this.native.openRoot(rootDirectory);\n this.rootHandle = handle;\n this.rootIdentity = this.native.queryIdentity(handle, 'inspect-root');\n } catch (error) {\n if (this.rootHandle !== undefined) {\n this.native.close(this.rootHandle);\n this.rootHandle = undefined;\n }\n if (error instanceof StableFileAuthorityError) throw error;\n throw authorityError('UNSUPPORTED_BACKEND', 'load-windows-backend');\n }\n }\n\n readBytes(\n relativeSegments: readonly string[],\n maxBytes: number,\n hooks: IFileAuthorityTestHooks,\n ): Uint8Array | undefined {\n const rootHandle = this.rootHandle;\n if (rootHandle === undefined) throw authorityError('AUTHORITY_CLOSED', 'read');\n this.assertRootIdentity(rootHandle);\n\n let parentHandle = rootHandle;\n let ownsParent = false;\n let fileHandle: TNativeHandle | undefined;\n try {\n for (let index = 0; index < relativeSegments.length; index += 1) {\n const final = index === relativeSegments.length - 1;\n const handle = this.native.openRelative(\n parentHandle,\n relativeSegments[index],\n final,\n index,\n );\n if (handle === undefined) return undefined;\n if (final) {\n fileHandle = handle;\n break;\n }\n if (ownsParent) this.native.close(parentHandle);\n parentHandle = handle;\n ownsParent = true;\n hooks.afterDirectoryOpened?.(index);\n }\n\n if (fileHandle === undefined) throw authorityError('HOST_IO', 'open-file');\n const bytes = this.native.readFile(fileHandle, maxBytes, hooks);\n this.assertRootIdentity(rootHandle);\n return bytes;\n } catch (error) {\n if (error instanceof StableFileAuthorityError) throw error;\n throw authorityError('HOST_IO', 'read');\n } finally {\n if (fileHandle !== undefined) this.native.close(fileHandle);\n if (ownsParent) this.native.close(parentHandle);\n }\n }\n\n close(): void {\n const handle = this.rootHandle;\n if (handle === undefined) return;\n this.rootHandle = undefined;\n this.native.close(handle);\n }\n\n private assertRootIdentity(handle: TNativeHandle): void {\n const current = this.native.queryIdentity(handle, 'inspect-root');\n if (\n current.volume !== this.rootIdentity.volume ||\n current.fileId !== this.rootIdentity.fileId\n ) {\n throw authorityError('ROOT_CHANGED', 'inspect-root');\n }\n }\n}\n","import { resolve } from 'node:path';\n\nimport {\n authorityError,\n StableFileAuthorityError,\n type IStableRootedFileReader,\n} from './contracts.js';\nimport { PosixStableFileHostBackend } from './posix-backend.js';\nimport { WindowsStableFileHostBackend } from './windows-backend.js';\n\nimport type { IFileAuthorityTestHooks, IStableFileHostBackend } from './host-backend.js';\n\nexport type { IFileAuthorityTestHooks } from './host-backend.js';\n\nconst SUPPORTED_HOSTS = new Set([\n 'darwin-arm64',\n 'darwin-x64',\n 'linux-arm64',\n 'linux-x64',\n 'win32-x64',\n]);\n\nconst finalizer = new FinalizationRegistry<IStableFileHostBackend>((backend) => {\n backend.close();\n});\n\nfunction validateRoot(rootDirectory: string): string {\n if (\n typeof rootDirectory !== 'string' ||\n rootDirectory.trim().length === 0 ||\n rootDirectory.includes('\\0')\n ) {\n throw authorityError('INVALID_PATH', 'validate-root');\n }\n return resolve(rootDirectory);\n}\n\nfunction validateReadInput(relativeSegments: readonly string[], maxBytes: number): void {\n if (!Array.isArray(relativeSegments) || relativeSegments.length === 0) {\n throw authorityError('INVALID_PATH', 'validate-segments');\n }\n for (let index = 0; index < relativeSegments.length; index += 1) {\n const segment = relativeSegments[index];\n if (\n typeof segment !== 'string' ||\n segment.length === 0 ||\n segment === '.' ||\n segment === '..' ||\n segment.includes('\\0') ||\n segment.includes('/') ||\n segment.includes('\\\\')\n ) {\n throw authorityError('INVALID_PATH', 'validate-segments', { segmentIndex: index });\n }\n }\n if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {\n throw authorityError('INVALID_PATH', 'validate-budget');\n }\n}\n\nfunction createHostBackend(rootDirectory: string): IStableFileHostBackend {\n const host = `${process.platform}-${process.arch}`;\n if (!SUPPORTED_HOSTS.has(host)) {\n throw authorityError('UNSUPPORTED_BACKEND', 'select-backend', { hostCode: host });\n }\n if (process.platform === 'win32') return new WindowsStableFileHostBackend(rootDirectory);\n return new PosixStableFileHostBackend(rootDirectory);\n}\n\nclass StableRootedFileReader implements IStableRootedFileReader {\n private backend: IStableFileHostBackend | undefined;\n private readonly finalizerToken = {};\n\n constructor(\n rootDirectory: string,\n private readonly hooks: IFileAuthorityTestHooks,\n ) {\n const backend = createHostBackend(validateRoot(rootDirectory));\n this.backend = backend;\n finalizer.register(this, backend, this.finalizerToken);\n }\n\n readBytes(relativeSegments: readonly string[], maxBytes: number): Uint8Array | undefined {\n const backend = this.backend;\n if (backend === undefined) throw authorityError('AUTHORITY_CLOSED', 'read');\n validateReadInput(relativeSegments, maxBytes);\n try {\n return backend.readBytes(relativeSegments, maxBytes, this.hooks);\n } catch (error) {\n if (error instanceof StableFileAuthorityError) throw error;\n throw authorityError('HOST_IO', 'read');\n }\n }\n\n close(): void {\n const backend = this.backend;\n if (backend === undefined) return;\n this.backend = undefined;\n finalizer.unregister(this.finalizerToken);\n try {\n backend.close();\n } catch (error) {\n if (error instanceof StableFileAuthorityError) throw error;\n throw authorityError('HOST_IO', 'close');\n }\n }\n\n [Symbol.dispose](): void {\n this.close();\n }\n}\n\n/** Create a stable, opaque read authority rooted at the currently opened directory identity. */\nexport function createStableRootedFileReader(rootDirectory: string): IStableRootedFileReader {\n return new StableRootedFileReader(rootDirectory, {});\n}\n\n/** Internal-only deterministic seams used by native replacement tests. */\nexport function createStableRootedFileReaderForTest(\n rootDirectory: string,\n hooks: IFileAuthorityTestHooks,\n): IStableRootedFileReader {\n return new StableRootedFileReader(rootDirectory, hooks);\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@robota-sdk/agent-file-authority",
|
|
3
|
+
"version": "3.0.0-beta.81",
|
|
4
|
+
"description": "Stable root-relative file read authority for Node.js",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/node/index.js",
|
|
7
|
+
"types": "dist/node/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"source": "./src/index.ts",
|
|
11
|
+
"node": {
|
|
12
|
+
"import": {
|
|
13
|
+
"types": "./dist/node/index.d.ts",
|
|
14
|
+
"default": "./dist/node/index.js"
|
|
15
|
+
},
|
|
16
|
+
"require": {
|
|
17
|
+
"types": "./dist/node/index.d.cts",
|
|
18
|
+
"default": "./dist/node/index.cjs"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"default": {
|
|
22
|
+
"import": {
|
|
23
|
+
"types": "./dist/node/index.d.ts",
|
|
24
|
+
"default": "./dist/node/index.js"
|
|
25
|
+
},
|
|
26
|
+
"require": {
|
|
27
|
+
"types": "./dist/node/index.d.cts",
|
|
28
|
+
"default": "./dist/node/index.cjs"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/woojubb/robota.git"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://robota.io/",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/woojubb/robota/issues"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=20.19.0"
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"dist",
|
|
46
|
+
"src"
|
|
47
|
+
],
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"koffi": "3.3.1"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^20.19.43",
|
|
53
|
+
"rimraf": "^5.0.10",
|
|
54
|
+
"tsdown": "^0.22.14",
|
|
55
|
+
"vitest": "^3.2.6"
|
|
56
|
+
},
|
|
57
|
+
"license": "AGPL-3.0-only OR LicenseRef-Commercial",
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"access": "public"
|
|
60
|
+
},
|
|
61
|
+
"volta": {
|
|
62
|
+
"extends": "../../package.json"
|
|
63
|
+
},
|
|
64
|
+
"scripts": {
|
|
65
|
+
"build": "rimraf dist && tsdown",
|
|
66
|
+
"build:js": "pnpm run build",
|
|
67
|
+
"build:types": "pnpm run build",
|
|
68
|
+
"test": "vitest run --passWithNoTests",
|
|
69
|
+
"test:coverage": "vitest run --coverage --passWithNoTests",
|
|
70
|
+
"typecheck": "tsgo --noEmit",
|
|
71
|
+
"lint": "eslint src/ --ext .ts",
|
|
72
|
+
"clean": "rimraf dist",
|
|
73
|
+
"qualify:native": "node scripts/qualify-native.mjs",
|
|
74
|
+
"qualify:bun:build": "bun scripts/build-native-qualification.mjs"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendFileSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
mkdtempSync,
|
|
5
|
+
renameSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
symlinkSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from 'node:fs';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
|
|
13
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
createStableRootedFileReaderForTest,
|
|
17
|
+
type IFileAuthorityTestHooks,
|
|
18
|
+
} from '../stable-rooted-file-reader.js';
|
|
19
|
+
import {
|
|
20
|
+
createStableRootedFileReader,
|
|
21
|
+
StableFileAuthorityError,
|
|
22
|
+
type TStableFileAuthorityErrorCode,
|
|
23
|
+
} from '../index.js';
|
|
24
|
+
|
|
25
|
+
const temporaryDirectories: string[] = [];
|
|
26
|
+
const readers: Array<{ close(): void }> = [];
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
for (const reader of readers.splice(0)) reader.close();
|
|
30
|
+
for (const directory of temporaryDirectories.splice(0)) {
|
|
31
|
+
rmSync(directory, { recursive: true, force: true });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
function track<T extends { close(): void }>(reader: T): T {
|
|
36
|
+
readers.push(reader);
|
|
37
|
+
return reader;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function fixture(): { root: string; outside: string; cleanupRoot: string } {
|
|
41
|
+
const cleanupRoot = mkdtempSync(join(tmpdir(), 'robota-file-authority-'));
|
|
42
|
+
temporaryDirectories.push(cleanupRoot);
|
|
43
|
+
const root = join(cleanupRoot, 'root');
|
|
44
|
+
const outside = join(cleanupRoot, 'outside');
|
|
45
|
+
mkdirSync(join(root, 'nested'), { recursive: true });
|
|
46
|
+
mkdirSync(outside, { recursive: true });
|
|
47
|
+
return { root, outside, cleanupRoot };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function expectCode(operation: () => unknown, code: TStableFileAuthorityErrorCode): void {
|
|
51
|
+
try {
|
|
52
|
+
operation();
|
|
53
|
+
} catch (error) {
|
|
54
|
+
expect(error).toBeInstanceOf(StableFileAuthorityError);
|
|
55
|
+
expect((error as StableFileAuthorityError).code).toBe(code);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
throw new Error(`Expected StableFileAuthorityError ${code}.`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe('stable rooted file reader', () => {
|
|
62
|
+
it('reads one nested regular file and represents only missing as undefined', () => {
|
|
63
|
+
const { root } = fixture();
|
|
64
|
+
writeFileSync(join(root, 'nested', 'payload.bin'), 'approved');
|
|
65
|
+
const reader = track(createStableRootedFileReader(root));
|
|
66
|
+
|
|
67
|
+
expect(Buffer.from(reader.readBytes(['nested', 'payload.bin'], 8) ?? []).toString()).toBe(
|
|
68
|
+
'approved',
|
|
69
|
+
);
|
|
70
|
+
expect(reader.readBytes(['nested', 'missing.bin'], 8)).toBeUndefined();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('validates every segment and byte budget before host I/O', () => {
|
|
74
|
+
const { root } = fixture();
|
|
75
|
+
const reader = track(createStableRootedFileReader(root));
|
|
76
|
+
|
|
77
|
+
for (const segments of [[], [''], ['.'], ['..'], ['a/b'], ['a\\b'], ['a\0b']]) {
|
|
78
|
+
expectCode(() => reader.readBytes(segments, 1), 'INVALID_PATH');
|
|
79
|
+
}
|
|
80
|
+
for (const budget of [-1, 0.5, Number.NaN, Number.POSITIVE_INFINITY]) {
|
|
81
|
+
expectCode(() => reader.readBytes(['missing'], budget), 'INVALID_PATH');
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('stays attached to the opened root when its original pathname is replaced', () => {
|
|
86
|
+
const { root, cleanupRoot } = fixture();
|
|
87
|
+
writeFileSync(join(root, 'nested', 'payload.bin'), 'approved');
|
|
88
|
+
const reader = track(createStableRootedFileReader(root));
|
|
89
|
+
|
|
90
|
+
const moved = join(cleanupRoot, 'root-original');
|
|
91
|
+
renameSync(root, moved);
|
|
92
|
+
mkdirSync(join(root, 'nested'), { recursive: true });
|
|
93
|
+
writeFileSync(join(root, 'nested', 'payload.bin'), 'retargeted');
|
|
94
|
+
|
|
95
|
+
expect(Buffer.from(reader.readBytes(['nested', 'payload.bin'], 32) ?? []).toString()).toBe(
|
|
96
|
+
'approved',
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('stays attached to an opened parent when that parent pathname is replaced mid-walk', () => {
|
|
101
|
+
const { root, outside } = fixture();
|
|
102
|
+
const original = join(root, 'nested');
|
|
103
|
+
const moved = join(root, 'nested-original');
|
|
104
|
+
writeFileSync(join(original, 'payload.bin'), 'approved');
|
|
105
|
+
writeFileSync(join(outside, 'payload.bin'), 'retargeted');
|
|
106
|
+
const hooks: IFileAuthorityTestHooks = {
|
|
107
|
+
afterDirectoryOpened: (index) => {
|
|
108
|
+
if (index !== 0) return;
|
|
109
|
+
renameSync(original, moved);
|
|
110
|
+
symlinkSync(outside, original, process.platform === 'win32' ? 'junction' : 'dir');
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
const reader = track(createStableRootedFileReaderForTest(root, hooks));
|
|
114
|
+
|
|
115
|
+
expect(Buffer.from(reader.readBytes(['nested', 'payload.bin'], 32) ?? []).toString()).toBe(
|
|
116
|
+
'approved',
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('refuses parent and final links or reparse points', () => {
|
|
121
|
+
const { root, outside } = fixture();
|
|
122
|
+
writeFileSync(join(outside, 'payload.bin'), 'outside-secret');
|
|
123
|
+
symlinkSync(
|
|
124
|
+
outside,
|
|
125
|
+
join(root, 'linked-parent'),
|
|
126
|
+
process.platform === 'win32' ? 'junction' : 'dir',
|
|
127
|
+
);
|
|
128
|
+
symlinkSync(
|
|
129
|
+
join(outside, 'payload.bin'),
|
|
130
|
+
join(root, 'linked-file.bin'),
|
|
131
|
+
process.platform === 'win32' ? 'file' : 'file',
|
|
132
|
+
);
|
|
133
|
+
const reader = track(createStableRootedFileReader(root));
|
|
134
|
+
|
|
135
|
+
expectCode(() => reader.readBytes(['linked-parent', 'payload.bin'], 64), 'UNSAFE_ENTRY');
|
|
136
|
+
expectCode(() => reader.readBytes(['linked-file.bin'], 64), 'UNSAFE_ENTRY');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('refuses non-regular final entries and enforces exact and zero-byte budgets', () => {
|
|
140
|
+
const { root } = fixture();
|
|
141
|
+
writeFileSync(join(root, 'empty.bin'), '');
|
|
142
|
+
writeFileSync(join(root, 'one.bin'), 'x');
|
|
143
|
+
const reader = track(createStableRootedFileReader(root));
|
|
144
|
+
|
|
145
|
+
expect(reader.readBytes(['empty.bin'], 0)?.byteLength).toBe(0);
|
|
146
|
+
expect(Buffer.from(reader.readBytes(['one.bin'], 1) ?? []).toString()).toBe('x');
|
|
147
|
+
expectCode(() => reader.readBytes(['one.bin'], 0), 'OVER_BUDGET');
|
|
148
|
+
expectCode(() => reader.readBytes(['nested'], 32), 'UNSAFE_ENTRY');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('detects deterministic growth during a read', () => {
|
|
152
|
+
const { root } = fixture();
|
|
153
|
+
const payload = join(root, 'payload.bin');
|
|
154
|
+
writeFileSync(payload, 'a');
|
|
155
|
+
let changed = false;
|
|
156
|
+
let writeBlocked = false;
|
|
157
|
+
const reader = track(
|
|
158
|
+
createStableRootedFileReaderForTest(root, {
|
|
159
|
+
afterReadChunk: () => {
|
|
160
|
+
if (changed) return;
|
|
161
|
+
changed = true;
|
|
162
|
+
try {
|
|
163
|
+
appendFileSync(payload, 'b');
|
|
164
|
+
} catch (error) {
|
|
165
|
+
if (process.platform !== 'win32') throw error;
|
|
166
|
+
writeBlocked = true;
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
if (process.platform === 'win32') {
|
|
173
|
+
expect(Buffer.from(reader.readBytes(['payload.bin'], 8) ?? []).toString()).toBe('a');
|
|
174
|
+
expect(writeBlocked).toBe(true);
|
|
175
|
+
} else {
|
|
176
|
+
expectCode(() => reader.readBytes(['payload.bin'], 8), 'FILE_CHANGED');
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('cannot return mixed bytes from a same-size rewrite during a multi-chunk read', () => {
|
|
181
|
+
const { root } = fixture();
|
|
182
|
+
const payload = join(root, 'payload.bin');
|
|
183
|
+
const bytes = Buffer.alloc(1_048_584, 'a');
|
|
184
|
+
writeFileSync(payload, bytes);
|
|
185
|
+
let rewriteAttempted = false;
|
|
186
|
+
let writeBlocked = false;
|
|
187
|
+
const reader = track(
|
|
188
|
+
createStableRootedFileReaderForTest(root, {
|
|
189
|
+
afterReadChunk: () => {
|
|
190
|
+
if (rewriteAttempted) return;
|
|
191
|
+
rewriteAttempted = true;
|
|
192
|
+
try {
|
|
193
|
+
writeFileSync(payload, Buffer.alloc(bytes.length, 'b'));
|
|
194
|
+
} catch (error) {
|
|
195
|
+
if (process.platform !== 'win32') throw error;
|
|
196
|
+
writeBlocked = true;
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
}),
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
if (process.platform === 'win32') {
|
|
203
|
+
expect(reader.readBytes(['payload.bin'], bytes.length)).toEqual(bytes);
|
|
204
|
+
expect(rewriteAttempted).toBe(true);
|
|
205
|
+
expect(writeBlocked).toBe(true);
|
|
206
|
+
} else {
|
|
207
|
+
expectCode(() => reader.readBytes(['payload.bin'], bytes.length), 'FILE_CHANGED');
|
|
208
|
+
expect(rewriteAttempted).toBe(true);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('closes idempotently and rejects every later operation', () => {
|
|
213
|
+
const { root } = fixture();
|
|
214
|
+
const reader = createStableRootedFileReader(root);
|
|
215
|
+
reader.close();
|
|
216
|
+
reader.close();
|
|
217
|
+
reader[Symbol.dispose]();
|
|
218
|
+
|
|
219
|
+
expectCode(() => reader.readBytes(['nested', 'missing.bin'], 1), 'AUTHORITY_CLOSED');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('keeps diagnostics free of absolute roots and file bytes', () => {
|
|
223
|
+
const { root } = fixture();
|
|
224
|
+
const secret = 'outside-secret-value';
|
|
225
|
+
writeFileSync(join(root, 'payload.bin'), secret);
|
|
226
|
+
const reader = track(createStableRootedFileReader(root));
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
reader.readBytes(['payload.bin'], 0);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
const rendered = JSON.stringify(error);
|
|
232
|
+
expect(rendered).not.toContain(root);
|
|
233
|
+
expect(rendered).not.toContain(secret);
|
|
234
|
+
expect(String(error)).not.toContain(root);
|
|
235
|
+
expect(String(error)).not.toContain(secret);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
throw new Error('Expected an over-budget error.');
|
|
239
|
+
});
|
|
240
|
+
});
|