@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.
@@ -0,0 +1,234 @@
1
+ import * as koffi from 'koffi';
2
+
3
+ export type TNativeHandle = bigint | null;
4
+
5
+ export interface IWindowsAttributeTagInfo {
6
+ FileAttributes?: number;
7
+ ReparseTag?: number;
8
+ }
9
+
10
+ export interface IWindowsFileIdInfo {
11
+ VolumeSerialNumber?: bigint | number;
12
+ FileId?: Uint8Array | number[];
13
+ }
14
+
15
+ export interface IWindowsStandardInfo {
16
+ Directory?: number;
17
+ }
18
+
19
+ export interface IWindowsObjectAttributes {
20
+ Length: number;
21
+ RootDirectory: TNativeHandle;
22
+ ObjectName: object;
23
+ Attributes: number;
24
+ SecurityDescriptor: null;
25
+ SecurityQualityOfService: null;
26
+ }
27
+
28
+ export interface IWindowsIoStatus {
29
+ Status: number;
30
+ Information: number;
31
+ }
32
+
33
+ type TCreateFileW = (
34
+ path: string,
35
+ desiredAccess: number,
36
+ shareMode: number,
37
+ securityAttributes: null,
38
+ creationDisposition: number,
39
+ flagsAndAttributes: number,
40
+ templateFile: TNativeHandle,
41
+ ) => TNativeHandle;
42
+
43
+ type TGetInformation = (
44
+ handle: TNativeHandle,
45
+ informationClass: number,
46
+ output: object,
47
+ outputBytes: number,
48
+ ) => boolean;
49
+
50
+ type TNtCreateFile = (
51
+ output: TNativeHandle[],
52
+ desiredAccess: number,
53
+ objectAttributes: IWindowsObjectAttributes,
54
+ ioStatus: IWindowsIoStatus,
55
+ allocationSize: null,
56
+ fileAttributes: number,
57
+ shareAccess: number,
58
+ createDisposition: number,
59
+ createOptions: number,
60
+ extendedAttributes: null,
61
+ extendedAttributesBytes: number,
62
+ ) => number;
63
+
64
+ export interface IWindowsApi {
65
+ readonly handleType: koffi.TypeObject;
66
+ readonly unicodeStringType: koffi.TypeObject;
67
+ readonly objectAttributesType: koffi.TypeObject;
68
+ readonly attributeTagInfoType: koffi.TypeObject;
69
+ readonly fileIdInfoType: koffi.TypeObject;
70
+ readonly standardInfoType: koffi.TypeObject;
71
+ readonly createFileW: TCreateFileW;
72
+ readonly closeHandle: (handle: TNativeHandle) => boolean;
73
+ readonly getAttributeTagInfo: TGetInformation;
74
+ readonly getFileIdInfo: TGetInformation;
75
+ readonly getStandardInfo: TGetInformation;
76
+ readonly getFileSizeEx: (handle: TNativeHandle, size: bigint[]) => boolean;
77
+ readonly readFile: (
78
+ handle: TNativeHandle,
79
+ buffer: Uint8Array,
80
+ bytesToRead: number,
81
+ bytesRead: number[],
82
+ overlapped: null,
83
+ ) => boolean;
84
+ readonly getLastError: () => number;
85
+ readonly ntCreateFile: TNtCreateFile;
86
+ readonly ntClose: (handle: TNativeHandle) => number;
87
+ }
88
+
89
+ interface IWindowsTypes {
90
+ readonly handleType: koffi.TypeObject;
91
+ readonly unicodeStringType: koffi.TypeObject;
92
+ readonly objectAttributesType: koffi.TypeObject;
93
+ readonly ioStatusBlockType: koffi.TypeObject;
94
+ readonly attributeTagInfoType: koffi.TypeObject;
95
+ readonly fileIdInfoType: koffi.TypeObject;
96
+ readonly standardInfoType: koffi.TypeObject;
97
+ }
98
+
99
+ const FILE_ID_BYTES = 16;
100
+
101
+ function defineWindowsTypes(): IWindowsTypes {
102
+ const handleType = koffi.pointer('RobotaStableFileHandle', koffi.opaque());
103
+ const unicodeStringType = koffi.struct('RobotaUnicodeString', {
104
+ Length: 'uint16_t',
105
+ MaximumLength: 'uint16_t',
106
+ Buffer: koffi.pointer('char16_t'),
107
+ });
108
+ const objectAttributesType = koffi.struct('RobotaObjectAttributes', {
109
+ Length: 'uint32_t',
110
+ RootDirectory: handleType,
111
+ ObjectName: koffi.pointer(unicodeStringType),
112
+ Attributes: 'uint32_t',
113
+ SecurityDescriptor: 'void *',
114
+ SecurityQualityOfService: 'void *',
115
+ });
116
+ return {
117
+ handleType,
118
+ unicodeStringType,
119
+ objectAttributesType,
120
+ ioStatusBlockType: koffi.struct('RobotaIoStatusBlock', {
121
+ Status: 'intptr_t',
122
+ Information: 'uintptr_t',
123
+ }),
124
+ attributeTagInfoType: koffi.struct('RobotaFileAttributeTagInfo', {
125
+ FileAttributes: 'uint32_t',
126
+ ReparseTag: 'uint32_t',
127
+ }),
128
+ fileIdInfoType: koffi.struct('RobotaFileIdInfo', {
129
+ VolumeSerialNumber: 'uint64_t',
130
+ FileId: koffi.array('uint8_t', FILE_ID_BYTES, 'Typed'),
131
+ }),
132
+ standardInfoType: koffi.struct('RobotaFileStandardInfo', {
133
+ AllocationSize: 'int64_t',
134
+ EndOfFile: 'int64_t',
135
+ NumberOfLinks: 'uint32_t',
136
+ DeletePending: 'uint8_t',
137
+ Directory: 'uint8_t',
138
+ }),
139
+ };
140
+ }
141
+
142
+ function bindKernel32(
143
+ library: koffi.LibraryHandle,
144
+ types: IWindowsTypes,
145
+ ): Pick<
146
+ IWindowsApi,
147
+ | 'createFileW'
148
+ | 'closeHandle'
149
+ | 'getAttributeTagInfo'
150
+ | 'getFileIdInfo'
151
+ | 'getStandardInfo'
152
+ | 'getFileSizeEx'
153
+ | 'readFile'
154
+ | 'getLastError'
155
+ > {
156
+ return {
157
+ createFileW: library.func('CreateFileW', types.handleType, [
158
+ koffi.pointer('char16_t'),
159
+ 'uint32_t',
160
+ 'uint32_t',
161
+ 'void *',
162
+ 'uint32_t',
163
+ 'uint32_t',
164
+ types.handleType,
165
+ ]) as TCreateFileW,
166
+ closeHandle: library.func('CloseHandle', 'bool', [types.handleType]),
167
+ ...bindInformationFunctions(library, types),
168
+ getFileSizeEx: library.func('GetFileSizeEx', 'bool', [
169
+ types.handleType,
170
+ koffi.out(koffi.pointer('int64_t')),
171
+ ]),
172
+ readFile: library.func('ReadFile', 'bool', [
173
+ types.handleType,
174
+ koffi.out(koffi.pointer('uint8_t')),
175
+ 'uint32_t',
176
+ koffi.out(koffi.pointer('uint32_t')),
177
+ 'void *',
178
+ ]),
179
+ getLastError: library.func('GetLastError', 'uint32_t', []),
180
+ };
181
+ }
182
+
183
+ function bindInformationFunctions(
184
+ library: koffi.LibraryHandle,
185
+ types: IWindowsTypes,
186
+ ): Pick<IWindowsApi, 'getAttributeTagInfo' | 'getFileIdInfo' | 'getStandardInfo'> {
187
+ const bind = (outputType: koffi.TypeObject): TGetInformation =>
188
+ library.func('GetFileInformationByHandleEx', 'bool', [
189
+ types.handleType,
190
+ 'int32_t',
191
+ koffi.out(koffi.pointer(outputType)),
192
+ 'uint32_t',
193
+ ]) as TGetInformation;
194
+ return {
195
+ getAttributeTagInfo: bind(types.attributeTagInfoType),
196
+ getFileIdInfo: bind(types.fileIdInfoType),
197
+ getStandardInfo: bind(types.standardInfoType),
198
+ };
199
+ }
200
+
201
+ function bindNtDll(
202
+ library: koffi.LibraryHandle,
203
+ types: IWindowsTypes,
204
+ ): Pick<IWindowsApi, 'ntCreateFile' | 'ntClose'> {
205
+ return {
206
+ ntCreateFile: library.func('NtCreateFile', 'int32_t', [
207
+ koffi.out(koffi.pointer(types.handleType)),
208
+ 'uint32_t',
209
+ koffi.pointer(types.objectAttributesType),
210
+ koffi.out(koffi.pointer(types.ioStatusBlockType)),
211
+ 'void *',
212
+ 'uint32_t',
213
+ 'uint32_t',
214
+ 'uint32_t',
215
+ 'uint32_t',
216
+ 'void *',
217
+ 'uint32_t',
218
+ ]) as TNtCreateFile,
219
+ ntClose: library.func('NtClose', 'int32_t', [types.handleType]),
220
+ };
221
+ }
222
+
223
+ let cachedApi: IWindowsApi | undefined;
224
+
225
+ export function getWindowsApi(): IWindowsApi {
226
+ if (cachedApi !== undefined) return cachedApi;
227
+ const types = defineWindowsTypes();
228
+ cachedApi = {
229
+ ...types,
230
+ ...bindKernel32(koffi.load('kernel32.dll'), types),
231
+ ...bindNtDll(koffi.load('ntdll.dll'), types),
232
+ };
233
+ return cachedApi;
234
+ }
@@ -0,0 +1,227 @@
1
+ import * as koffi from 'koffi';
2
+
3
+ import { authorityError } from './contracts.js';
4
+ import {
5
+ getWindowsApi,
6
+ type IWindowsApi,
7
+ type IWindowsAttributeTagInfo,
8
+ type IWindowsIoStatus,
9
+ type IWindowsObjectAttributes,
10
+ type IWindowsStandardInfo,
11
+ type TNativeHandle,
12
+ } from './windows-native-api.js';
13
+ import {
14
+ queryWindowsIdentity,
15
+ readWindowsFile,
16
+ windowsLastErrorCode,
17
+ type IWindowsIdentity,
18
+ } from './windows-native-read.js';
19
+ import { classifyNtOpenFailure } from './windows-native-status.js';
20
+
21
+ import type { IFileAuthorityTestHooks } from './host-backend.js';
22
+
23
+ export type { IWindowsIdentity } from './windows-native-read.js';
24
+
25
+ const GENERIC_READ = 0x80000000;
26
+ const FILE_READ_DATA = 0x0001;
27
+ const FILE_LIST_DIRECTORY = 0x0001;
28
+ const FILE_READ_ATTRIBUTES = 0x0080;
29
+ const SYNCHRONIZE = 0x00100000;
30
+ const FILE_SHARE_READ = 0x00000001;
31
+ const FILE_SHARE_WRITE = 0x00000002;
32
+ const FILE_SHARE_DELETE = 0x00000004;
33
+ const OPEN_EXISTING = 3;
34
+ const FILE_ATTRIBUTE_REPARSE_POINT = 0x00000400;
35
+ const FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000;
36
+ const FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
37
+ const FILE_OPEN = 1;
38
+ const FILE_DIRECTORY_FILE = 0x00000001;
39
+ const FILE_SYNCHRONOUS_IO_NONALERT = 0x00000020;
40
+ const FILE_NON_DIRECTORY_FILE = 0x00000040;
41
+ const FILE_OPEN_REPARSE_POINT = 0x00200000;
42
+ const OBJ_CASE_INSENSITIVE = 0x00000040;
43
+ const OBJ_DONT_REPARSE = 0x00001000;
44
+ const FILE_STANDARD_INFO_CLASS = 1;
45
+ const FILE_ATTRIBUTE_TAG_INFO_CLASS = 9;
46
+ const HANDLE_WIDTH_BITS = 64;
47
+ const INVALID_HANDLE_VALUE = BigInt('-1');
48
+
49
+ function isInvalidHandle(handle: TNativeHandle): boolean {
50
+ return handle === null || BigInt.asIntN(HANDLE_WIDTH_BITS, handle) === INVALID_HANDLE_VALUE;
51
+ }
52
+
53
+ /** Raw Windows handle operations. Native HANDLE and Koffi values stay inside this module. */
54
+ export class WindowsNativeAuthority {
55
+ private readonly api: IWindowsApi;
56
+
57
+ constructor() {
58
+ try {
59
+ this.api = getWindowsApi();
60
+ } catch {
61
+ throw authorityError('UNSUPPORTED_BACKEND', 'load-windows-backend');
62
+ }
63
+ }
64
+
65
+ openRoot(rootDirectory: string): TNativeHandle {
66
+ const handle = this.api.createFileW(
67
+ rootDirectory,
68
+ GENERIC_READ,
69
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
70
+ null,
71
+ OPEN_EXISTING,
72
+ FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
73
+ null,
74
+ );
75
+ if (isInvalidHandle(handle)) {
76
+ throw authorityError('HOST_IO', 'open-root', { hostCode: this.lastErrorCode() });
77
+ }
78
+ try {
79
+ this.assertSafeHandle(handle, true, 'inspect-root');
80
+ return handle;
81
+ } catch (error) {
82
+ this.close(handle);
83
+ throw error;
84
+ }
85
+ }
86
+
87
+ openRelative(
88
+ parentHandle: TNativeHandle,
89
+ segment: string,
90
+ final: boolean,
91
+ segmentIndex: number,
92
+ ): TNativeHandle | undefined {
93
+ const { handle, status } = this.issueRelativeOpen(parentHandle, segment, final);
94
+ if (status >= 0) {
95
+ if (isInvalidHandle(handle)) throw authorityError('HOST_IO', 'open-relative');
96
+ try {
97
+ this.assertSafeHandle(
98
+ handle,
99
+ !final,
100
+ final ? 'inspect-file' : 'inspect-directory',
101
+ segmentIndex,
102
+ );
103
+ return handle;
104
+ } catch (error) {
105
+ this.close(handle);
106
+ throw error;
107
+ }
108
+ }
109
+ if (!isInvalidHandle(handle)) this.close(handle);
110
+ const failure = classifyNtOpenFailure(status);
111
+ if (failure.kind === 'missing') return undefined;
112
+ const details = { segmentIndex, hostCode: failure.hostCode };
113
+ if (failure.kind === 'unsafe') {
114
+ throw authorityError('UNSAFE_ENTRY', final ? 'open-file' : 'open-directory', details);
115
+ }
116
+ throw authorityError('HOST_IO', final ? 'open-file' : 'open-directory', details);
117
+ }
118
+
119
+ readFile(handle: TNativeHandle, maxBytes: number, hooks: IFileAuthorityTestHooks): Uint8Array {
120
+ return readWindowsFile(this.api, handle, maxBytes, hooks);
121
+ }
122
+
123
+ queryIdentity(handle: TNativeHandle, operation: string): IWindowsIdentity {
124
+ return queryWindowsIdentity(this.api, handle, operation);
125
+ }
126
+
127
+ close(handle: TNativeHandle): void {
128
+ try {
129
+ if (this.api.ntClose(handle) >= 0) return;
130
+ } catch {
131
+ // allow-fallback: CloseHandle is the native cleanup fallback after NtClose itself throws
132
+ }
133
+ try {
134
+ this.api.closeHandle(handle);
135
+ } catch {
136
+ // allow-fallback: close failure cannot restore authority or make a later read safe
137
+ }
138
+ }
139
+
140
+ private issueRelativeOpen(
141
+ parentHandle: TNativeHandle,
142
+ segment: string,
143
+ final: boolean,
144
+ ): { handle: TNativeHandle; status: number } {
145
+ const unicodeName = koffi.alloc(this.api.unicodeStringType, 1);
146
+ try {
147
+ koffi.encode(unicodeName, this.api.unicodeStringType, {
148
+ Length: Buffer.byteLength(segment, 'utf16le'),
149
+ MaximumLength: Buffer.byteLength(`${segment}\0`, 'utf16le'),
150
+ Buffer: segment,
151
+ });
152
+ const attributes: IWindowsObjectAttributes = {
153
+ Length: koffi.sizeof(this.api.objectAttributesType),
154
+ RootDirectory: parentHandle,
155
+ ObjectName: unicodeName,
156
+ Attributes: OBJ_CASE_INSENSITIVE | OBJ_DONT_REPARSE,
157
+ SecurityDescriptor: null,
158
+ SecurityQualityOfService: null,
159
+ };
160
+ const output: TNativeHandle[] = [null];
161
+ const ioStatus: IWindowsIoStatus = { Status: 0, Information: 0 };
162
+ // A final read cannot verify same-size in-place rewrites from identity and size alone.
163
+ // Deny write sharing for that handle so existing and new writers cannot race the read.
164
+ const shareAccess = final
165
+ ? FILE_SHARE_READ | FILE_SHARE_DELETE
166
+ : FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;
167
+ const status = this.api.ntCreateFile(
168
+ output,
169
+ (final ? FILE_READ_DATA : FILE_LIST_DIRECTORY) | FILE_READ_ATTRIBUTES | SYNCHRONIZE,
170
+ attributes,
171
+ ioStatus,
172
+ null,
173
+ 0,
174
+ shareAccess,
175
+ FILE_OPEN,
176
+ (final ? FILE_NON_DIRECTORY_FILE : FILE_DIRECTORY_FILE) |
177
+ FILE_SYNCHRONOUS_IO_NONALERT |
178
+ FILE_OPEN_REPARSE_POINT,
179
+ null,
180
+ 0,
181
+ );
182
+ return { handle: output[0], status };
183
+ } finally {
184
+ koffi.free(unicodeName);
185
+ }
186
+ }
187
+
188
+ private assertSafeHandle(
189
+ handle: TNativeHandle,
190
+ directory: boolean,
191
+ operation: string,
192
+ segmentIndex?: number,
193
+ ): void {
194
+ const attributes: IWindowsAttributeTagInfo = {};
195
+ const standard: IWindowsStandardInfo = {};
196
+ if (
197
+ !this.api.getAttributeTagInfo(
198
+ handle,
199
+ FILE_ATTRIBUTE_TAG_INFO_CLASS,
200
+ attributes,
201
+ koffi.sizeof(this.api.attributeTagInfoType),
202
+ ) ||
203
+ !this.api.getStandardInfo(
204
+ handle,
205
+ FILE_STANDARD_INFO_CLASS,
206
+ standard,
207
+ koffi.sizeof(this.api.standardInfoType),
208
+ )
209
+ ) {
210
+ throw authorityError('HOST_IO', operation, { hostCode: this.lastErrorCode() });
211
+ }
212
+ const unsafe =
213
+ ((attributes.FileAttributes ?? 0) & FILE_ATTRIBUTE_REPARSE_POINT) !== 0 ||
214
+ Boolean(standard.Directory) !== directory;
215
+ if (unsafe) {
216
+ throw authorityError(
217
+ 'UNSAFE_ENTRY',
218
+ operation,
219
+ segmentIndex === undefined ? {} : { segmentIndex },
220
+ );
221
+ }
222
+ }
223
+
224
+ private lastErrorCode(): string {
225
+ return windowsLastErrorCode(this.api);
226
+ }
227
+ }
@@ -0,0 +1,122 @@
1
+ import * as koffi from 'koffi';
2
+
3
+ import { authorityError } from './contracts.js';
4
+
5
+ import type { IFileAuthorityTestHooks } from './host-backend.js';
6
+ import type { IWindowsApi, IWindowsFileIdInfo, TNativeHandle } from './windows-native-api.js';
7
+
8
+ export interface IWindowsIdentity {
9
+ readonly volume: bigint;
10
+ readonly fileId: string;
11
+ }
12
+
13
+ interface IWindowsFileSnapshot extends IWindowsIdentity {
14
+ readonly size: bigint;
15
+ }
16
+
17
+ const FILE_ID_INFO_CLASS = 18;
18
+ const CHUNK_BYTES = 1_048_576;
19
+ const ZERO_SIZE = BigInt('0');
20
+
21
+ function bytesToHex(value: Uint8Array | number[] | undefined): string {
22
+ if (value instanceof Uint8Array) return Buffer.from(value).toString('hex');
23
+ if (Array.isArray(value)) return Buffer.from(value).toString('hex');
24
+ throw authorityError('UNSUPPORTED_BACKEND', 'decode-file-identity');
25
+ }
26
+
27
+ export function windowsLastErrorCode(api: IWindowsApi): string {
28
+ return `WIN32_${api.getLastError()}`;
29
+ }
30
+
31
+ export function queryWindowsIdentity(
32
+ api: IWindowsApi,
33
+ handle: TNativeHandle,
34
+ operation: string,
35
+ ): IWindowsIdentity {
36
+ const info: IWindowsFileIdInfo = {};
37
+ if (!api.getFileIdInfo(handle, FILE_ID_INFO_CLASS, info, koffi.sizeof(api.fileIdInfoType))) {
38
+ throw authorityError('UNSUPPORTED_BACKEND', operation, {
39
+ hostCode: windowsLastErrorCode(api),
40
+ });
41
+ }
42
+ return {
43
+ volume: BigInt(info.VolumeSerialNumber ?? ZERO_SIZE),
44
+ fileId: bytesToHex(info.FileId),
45
+ };
46
+ }
47
+
48
+ export function readWindowsFile(
49
+ api: IWindowsApi,
50
+ handle: TNativeHandle,
51
+ maxBytes: number,
52
+ hooks: IFileAuthorityTestHooks,
53
+ ): Uint8Array {
54
+ const before = queryFileSnapshot(api, handle);
55
+ if (before.size > BigInt(maxBytes)) throw authorityError('OVER_BUDGET', 'read-file');
56
+ const expectedBytes = Number(before.size);
57
+ let bytes: Buffer;
58
+ try {
59
+ bytes = Buffer.alloc(expectedBytes);
60
+ } catch {
61
+ throw authorityError('HOST_IO', 'allocate-read-buffer');
62
+ }
63
+
64
+ const offset = readExpectedBytes(api, handle, bytes, hooks);
65
+ const probeCount = [0];
66
+ if (!api.readFile(handle, Buffer.alloc(1), 1, probeCount, null)) {
67
+ throw authorityError('HOST_IO', 'probe-file', { hostCode: windowsLastErrorCode(api) });
68
+ }
69
+ const after = queryFileSnapshot(api, handle);
70
+ if (probeCount[0] !== 0 && expectedBytes >= maxBytes) {
71
+ throw authorityError('OVER_BUDGET', 'read-file');
72
+ }
73
+ if (!isStableRead(before, after, offset, expectedBytes, probeCount[0])) {
74
+ throw authorityError('FILE_CHANGED', 'read-file');
75
+ }
76
+ return bytes;
77
+ }
78
+
79
+ function readExpectedBytes(
80
+ api: IWindowsApi,
81
+ handle: TNativeHandle,
82
+ bytes: Buffer,
83
+ hooks: IFileAuthorityTestHooks,
84
+ ): number {
85
+ let offset = 0;
86
+ while (offset < bytes.length) {
87
+ const length = Math.min(CHUNK_BYTES, bytes.length - offset);
88
+ const count = [0];
89
+ if (!api.readFile(handle, bytes.subarray(offset, offset + length), length, count, null)) {
90
+ throw authorityError('HOST_IO', 'read-file', { hostCode: windowsLastErrorCode(api) });
91
+ }
92
+ if (count[0] === 0) break;
93
+ offset += count[0];
94
+ hooks.afterReadChunk?.(offset);
95
+ }
96
+ return offset;
97
+ }
98
+
99
+ function queryFileSnapshot(api: IWindowsApi, handle: TNativeHandle): IWindowsFileSnapshot {
100
+ const identity = queryWindowsIdentity(api, handle, 'inspect-file');
101
+ const size = [ZERO_SIZE];
102
+ if (!api.getFileSizeEx(handle, size)) {
103
+ throw authorityError('HOST_IO', 'inspect-file', { hostCode: windowsLastErrorCode(api) });
104
+ }
105
+ return { ...identity, size: BigInt(size[0]) };
106
+ }
107
+
108
+ function isStableRead(
109
+ before: IWindowsFileSnapshot,
110
+ after: IWindowsFileSnapshot,
111
+ actualBytes: number,
112
+ expectedBytes: number,
113
+ probeBytes: number,
114
+ ): boolean {
115
+ return (
116
+ actualBytes === expectedBytes &&
117
+ probeBytes === 0 &&
118
+ before.volume === after.volume &&
119
+ before.fileId === after.fileId &&
120
+ before.size === after.size
121
+ );
122
+ }
@@ -0,0 +1,41 @@
1
+ type TNtOpenFailureKind = 'missing' | 'unsafe' | 'host-io';
2
+
3
+ export interface INtOpenFailure {
4
+ readonly kind: TNtOpenFailureKind;
5
+ readonly hostCode: string;
6
+ }
7
+
8
+ const HEX_RADIX = 16;
9
+ const STATUS_NO_SUCH_FILE = 0xc000000f;
10
+ const STATUS_OBJECT_NAME_NOT_FOUND = 0xc0000034;
11
+ const STATUS_OBJECT_PATH_NOT_FOUND = 0xc000003a;
12
+ const STATUS_STOPPED_ON_SYMLINK = 0x8000002d;
13
+ const STATUS_FILE_IS_A_DIRECTORY = 0xc00000ba;
14
+ const STATUS_NOT_A_DIRECTORY = 0xc0000103;
15
+ const STATUS_REPARSE_POINT_ENCOUNTERED = 0xc0000279;
16
+ const STATUS_IO_REPARSE_TAG_NOT_HANDLED = 0xc000050b;
17
+
18
+ const MISSING_STATUSES = new Set([
19
+ STATUS_NO_SUCH_FILE,
20
+ STATUS_OBJECT_NAME_NOT_FOUND,
21
+ STATUS_OBJECT_PATH_NOT_FOUND,
22
+ ]);
23
+ const UNSAFE_STATUSES = new Set([
24
+ STATUS_STOPPED_ON_SYMLINK,
25
+ STATUS_FILE_IS_A_DIRECTORY,
26
+ STATUS_NOT_A_DIRECTORY,
27
+ STATUS_REPARSE_POINT_ENCOUNTERED,
28
+ STATUS_IO_REPARSE_TAG_NOT_HANDLED,
29
+ ]);
30
+
31
+ export function classifyNtOpenFailure(status: number): INtOpenFailure {
32
+ const normalized = status >>> 0;
33
+ return {
34
+ kind: MISSING_STATUSES.has(normalized)
35
+ ? 'missing'
36
+ : UNSAFE_STATUSES.has(normalized)
37
+ ? 'unsafe'
38
+ : 'host-io',
39
+ hostCode: `NTSTATUS_0x${normalized.toString(HEX_RADIX)}`,
40
+ };
41
+ }