@k13engineering/linux-fuse 0.0.7 → 0.1.1

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.
@@ -26,14 +26,30 @@ const align = ({ value, alignment }/*: { value: number; alignment: number }*/)/*
26
26
  // eslint-disable-next-line complexity
27
27
  const direntFileTypeToNative = ({ fileType }/*: { fileType: TDirentFileType }*/)/*: number*/ => {
28
28
  switch (fileType) {
29
- case "DT_FIFO": return 1;
30
- case "DT_CHR": return 2;
31
- case "DT_DIR": return 4;
32
- case "DT_BLK": return 6;
33
- case "DT_REG": return 8;
34
- case "DT_LNK": return 10;
35
- case "DT_SOCK": return 12;
36
- default: return 0;
29
+ case "DT_FIFO": {
30
+ return 1;
31
+ }
32
+ case "DT_CHR": {
33
+ return 2;
34
+ }
35
+ case "DT_DIR": {
36
+ return 4;
37
+ }
38
+ case "DT_BLK": {
39
+ return 6;
40
+ }
41
+ case "DT_REG": {
42
+ return 8;
43
+ }
44
+ case "DT_LNK": {
45
+ return 10;
46
+ }
47
+ case "DT_SOCK": {
48
+ return 12;
49
+ }
50
+ default: {
51
+ return 0;
52
+ }
37
53
  }
38
54
  };
39
55
 
@@ -0,0 +1,41 @@
1
+ import { type TRequestHandler } from "./server.js";
2
+ import { type TMountAttributes, type TRequiredFuseMountOptions } from "./mount.js";
3
+ import type { TKernel } from "./kernel.js";
4
+ import type { TSyscalls } from "./syscalls.js";
5
+ type TFuseFileSystemOptions = {
6
+ fuseFd: number;
7
+ requestHandler: TRequestHandler;
8
+ sniff?: {
9
+ rawMessageFromKernel: (args: {
10
+ data: Uint8Array;
11
+ }) => void;
12
+ rawMessageToKernel: (args: {
13
+ data: Uint8Array;
14
+ }) => void;
15
+ };
16
+ };
17
+ type TFsMountResult = {
18
+ error: Error;
19
+ mountFd: undefined;
20
+ } | {
21
+ error: undefined;
22
+ mountFd: number;
23
+ };
24
+ type TMountOptions = {
25
+ otherOptions: {
26
+ default_permissions?: boolean;
27
+ allow_other?: boolean;
28
+ allow_root?: boolean;
29
+ } | Record<string, string | boolean>;
30
+ requiredOptions: TRequiredFuseMountOptions;
31
+ mountAttributes: TMountAttributes;
32
+ };
33
+ declare const createFuseFileSystem: ({ syscalls, createPoller, fuseFd, requestHandler, sniff, }: TFuseFileSystemOptions & {
34
+ syscalls: TSyscalls;
35
+ createPoller: TKernel["createPoller"];
36
+ }) => {
37
+ mountDetached: ({ otherOptions, requiredOptions, mountAttributes, }: TMountOptions) => TFsMountResult;
38
+ close: () => void;
39
+ };
40
+ export { createFuseFileSystem, };
41
+ export type { TFuseFileSystemOptions, };
@@ -0,0 +1,344 @@
1
+ import { createFuseLowLevel } from "./fuse-lowlevel.js";
2
+ import { createFuseServer, /*type TRequestHandler */} from "./server.js";
3
+ import {
4
+ FSCONFIG_CMD_CREATE,
5
+ FSCONFIG_SET_FLAG,
6
+ FSCONFIG_SET_STRING,
7
+ FSMOUNT_CLOEXEC,
8
+ FSOPEN_CLOEXEC,
9
+ FUSE_MIN_READ_BUFFER,
10
+ } from "./constants.js";
11
+ import {
12
+ mountAttributesToBitmask,
13
+ /*type TMountAttributes,
14
+ *//*type TRequiredFuseMountOptions
15
+ */} from "./mount.js";
16
+ /*import type { TKernel } from "./kernel.ts";*/
17
+ /*import type { TSyscalls } from "./syscalls.ts";*/
18
+
19
+ const UV_EBADF = -9;
20
+
21
+ const MAX_ALLOWED_WRITE = 32n * 1024n * 1024n;
22
+
23
+ /*type TFuseFileSystemOptions = {
24
+ fuseFd: number;
25
+ requestHandler: TRequestHandler;
26
+
27
+ sniff?: {
28
+ rawMessageFromKernel: (args: { data: Uint8Array }) => void;
29
+ rawMessageToKernel: (args: { data: Uint8Array }) => void;
30
+ }
31
+ };*/
32
+
33
+ /*type TFsMountResult = {
34
+ error: Error;
35
+ mountFd: undefined;
36
+ } | {
37
+ error: undefined;
38
+ mountFd: number;
39
+ };*/
40
+
41
+ /*type TMountOptions = {
42
+ otherOptions: {
43
+ default_permissions?: boolean;
44
+ allow_other?: boolean;
45
+ allow_root?: boolean;
46
+ } | Record<string, string | boolean>;
47
+ requiredOptions: TRequiredFuseMountOptions;
48
+ mountAttributes: TMountAttributes;
49
+ };*/
50
+
51
+ const forbiddenOptions = ["fd", "rootmode", "user_id", "group_id"];
52
+
53
+ const closeFdOrThrow = ({ syscalls, fd }/*: { syscalls: Pick<TSyscalls, "close">; fd: number }*/) => {
54
+ const { errno } = syscalls.close({ fd });
55
+ if (errno !== undefined) {
56
+ throw Error(`close syscall failed with errno ${errno}`);
57
+ }
58
+ };
59
+
60
+ const createFuseFileSystem = ({
61
+ syscalls,
62
+ createPoller,
63
+ fuseFd,
64
+ requestHandler,
65
+ sniff,
66
+ }/*: TFuseFileSystemOptions & {
67
+ syscalls: TSyscalls;
68
+ createPoller: TKernel["createPoller"];
69
+ }*/) => {
70
+
71
+ let closed = false;
72
+
73
+ let lowLevel = createFuseLowLevel({
74
+ syscalls,
75
+ fuseFd,
76
+ maxMessageSize: 1 * 1024 * 1024 + 4096,
77
+ });
78
+
79
+ const server = createFuseServer({
80
+ requestHandler: {
81
+ // eslint-disable-next-line complexity
82
+ handleRequest: async ({ request }) => {
83
+ const response = await requestHandler.handleRequest({ request });
84
+
85
+ if (response !== undefined && response.forOpcode === "INIT" && response.errorCode === undefined) {
86
+
87
+ if (response.result.maxWrite >= MAX_ALLOWED_WRITE) {
88
+ throw Error(`maxWrite of ${response.result.maxWrite} is too large, refusing to operate`);
89
+ }
90
+
91
+ lowLevel = createFuseLowLevel({
92
+ syscalls,
93
+ fuseFd,
94
+ // the kernel rejects reads into buffers smaller than FUSE_MIN_READ_BUFFER
95
+ maxMessageSize: Math.max(FUSE_MIN_READ_BUFFER, Number(response.result.maxWrite) + 4096),
96
+ });
97
+ }
98
+
99
+ return response;
100
+ }
101
+ },
102
+ sendMessageToKernel: ({ rawMessageToKernel }) => {
103
+ if (closed) {
104
+ // a request finished after close, its file descriptor number may already be reused
105
+ return;
106
+ }
107
+
108
+ sniff?.rawMessageToKernel({ data: rawMessageToKernel });
109
+ lowLevel.writeOutgoingMessage({
110
+ rawMessage: rawMessageToKernel
111
+ });
112
+ }
113
+ });
114
+
115
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
116
+ let ended = false;
117
+
118
+ const poller = createPoller({
119
+ fd: fuseFd
120
+ });
121
+
122
+ const handlePollError = ({ errorCode }/*: { errorCode: number }*/) => {
123
+ if (errorCode === UV_EBADF) {
124
+ // EBADF is raised when the kernel unmounts the filesystem
125
+ ended = true;
126
+ return;
127
+ }
128
+
129
+ throw Error(`poll error with code ${errorCode}`);
130
+ };
131
+
132
+ // reads and processes one message, returns whether the fuse device should be polled further
133
+ // eslint-disable-next-line complexity
134
+ const processIncomingMessage = () => {
135
+
136
+ if (closed) {
137
+ throw Error("BUG: should not be called after close");
138
+ }
139
+
140
+ const { error: readError, incomingMessage, end } = lowLevel.tryReadIncomingMessage();
141
+ if (readError !== undefined) {
142
+ throw readError;
143
+ }
144
+
145
+ if (end) {
146
+ ended = true;
147
+ return false;
148
+ }
149
+
150
+ if (incomingMessage !== undefined) {
151
+ sniff?.rawMessageFromKernel({ data: incomingMessage });
152
+ server.processRequest({
153
+ rawIncomingRequest: incomingMessage
154
+ });
155
+ }
156
+
157
+ return true;
158
+ };
159
+
160
+ const armPoller = () => {
161
+ poller.armOnce({
162
+ error: ({ errorCode }) => {
163
+ handlePollError({ errorCode });
164
+ },
165
+
166
+ readable: () => {
167
+ if (processIncomingMessage()) {
168
+ armPoller();
169
+ }
170
+ }
171
+ });
172
+ };
173
+
174
+ let polling = false;
175
+
176
+ // the kernel reports a fuse device that is not mounted yet as erroneous to poll, which looks the
177
+ // same as an unmount, so polling only starts once the device is connected by mountDetached
178
+ const startPolling = () => {
179
+ if (polling) {
180
+ return;
181
+ }
182
+
183
+ polling = true;
184
+ armPoller();
185
+ };
186
+
187
+ const configureAndMount = ({
188
+ fsfd,
189
+ otherOptions,
190
+ requiredOptions,
191
+ mountAttributesBitmask,
192
+ }/*: Omit<TMountOptions, "mountAttributes"> & {
193
+ fsfd: number;
194
+ mountAttributesBitmask: bigint;
195
+ }*/)/*: TFsMountResult*/ => {
196
+
197
+ const configureString = ({ key, value }/*: { key: string; value: string }*/) => {
198
+ const { errno } = syscalls.fsconfig({
199
+ fd: fsfd,
200
+ command: FSCONFIG_SET_STRING,
201
+ key,
202
+ value,
203
+ auxiliaryValue: 0,
204
+ });
205
+
206
+ if (errno !== undefined) {
207
+ throw Error(`fsconfig syscall failed with errno ${errno} when setting ${key}=${value}`);
208
+ }
209
+ };
210
+
211
+ const configureFlag = ({ key, value }/*: { key: string; value: boolean }*/) => {
212
+ if (!value) {
213
+ return;
214
+ }
215
+
216
+ const { errno } = syscalls.fsconfig({
217
+ fd: fsfd,
218
+ command: FSCONFIG_SET_FLAG,
219
+ key,
220
+ value: undefined,
221
+ auxiliaryValue: 0,
222
+ });
223
+
224
+ if (errno !== undefined) {
225
+ throw Error(`fsconfig syscall failed with errno ${errno} when setting flag ${key}`);
226
+ }
227
+ };
228
+
229
+ Object.keys(otherOptions).forEach((key) => {
230
+ const value = otherOptions[key /*as keyof typeof otherOptions*/];
231
+
232
+ if (value === undefined) {
233
+ return;
234
+ }
235
+
236
+ if (typeof value === "boolean") {
237
+ configureFlag({ key, value });
238
+ } else {
239
+ configureString({ key, value });
240
+ }
241
+ });
242
+
243
+ configureString({ key: "fd", value: `${fuseFd}` });
244
+ configureString({ key: "rootmode", value: `${requiredOptions.rootmode.toString(8)}` });
245
+ configureString({ key: "user_id", value: `${requiredOptions.user_id}` });
246
+ configureString({ key: "group_id", value: `${requiredOptions.group_id}` });
247
+
248
+ const { errno: createErrno } = syscalls.fsconfig({
249
+ fd: fsfd,
250
+ command: FSCONFIG_CMD_CREATE,
251
+ key: undefined,
252
+ value: undefined,
253
+ auxiliaryValue: 0,
254
+ });
255
+ if (createErrno !== undefined) {
256
+ return {
257
+ error: Error(`fsconfig CMD_CREATE syscall failed with errno ${createErrno}`),
258
+ mountFd: undefined,
259
+ };
260
+ }
261
+
262
+ const { errno: mountErrno, mountFd } = syscalls.fsmount({
263
+ fd: fsfd,
264
+ flags: FSMOUNT_CLOEXEC,
265
+ mountAttributesBitmask,
266
+ });
267
+
268
+ if (mountErrno !== undefined) {
269
+ return {
270
+ error: Error(`fsmount syscall failed with errno ${mountErrno}`),
271
+ mountFd: undefined,
272
+ };
273
+ }
274
+
275
+ return {
276
+ error: undefined,
277
+ mountFd,
278
+ };
279
+ };
280
+
281
+ const mountDetached = ({
282
+ otherOptions,
283
+ requiredOptions,
284
+ mountAttributes,
285
+ }/*: TMountOptions*/)/*: TFsMountResult*/ => {
286
+
287
+ // validate everything that can be validated before creating the filesystem context
288
+ Object.keys(otherOptions).forEach((key) => {
289
+ if (forbiddenOptions.includes(key)) {
290
+ throw Error(`option ${key} is not allowed as it is required for correct mounting`);
291
+ }
292
+ });
293
+
294
+ const mountAttributesBitmask = mountAttributesToBitmask({ mountAttributes });
295
+
296
+ const { errno: fsopenErrno, fsfd } = syscalls.fsopen({
297
+ fileSystemName: "fuse",
298
+ flags: FSOPEN_CLOEXEC
299
+ });
300
+
301
+ if (fsopenErrno !== undefined) {
302
+ return {
303
+ error: Error(`fsopen syscall failed with errno ${fsopenErrno}`),
304
+ mountFd: undefined,
305
+ };
306
+ }
307
+
308
+ try {
309
+ const result = configureAndMount({ fsfd, otherOptions, requiredOptions, mountAttributesBitmask });
310
+ if (result.error === undefined) {
311
+ startPolling();
312
+ }
313
+
314
+ return result;
315
+ } finally {
316
+ // the mount keeps its own reference, so the filesystem context is not needed afterwards
317
+ closeFdOrThrow({ syscalls, fd: fsfd });
318
+ }
319
+ };
320
+
321
+ const close = () => {
322
+ if (closed) {
323
+ throw Error("already closed");
324
+ }
325
+
326
+ closed = true;
327
+
328
+ poller.close();
329
+ closeFdOrThrow({ syscalls, fd: fuseFd });
330
+ };
331
+
332
+ return {
333
+ mountDetached,
334
+ close,
335
+ };
336
+ };
337
+
338
+ export {
339
+ createFuseFileSystem,
340
+ };
341
+
342
+ /*export type {
343
+ TFuseFileSystemOptions,
344
+ };*/
@@ -1,3 +1,4 @@
1
+ import type { TSyscalls } from "./syscalls.js";
1
2
  type TIncomingRequest = {
2
3
  header: {
3
4
  opcode: bigint;
@@ -29,7 +30,8 @@ type TTryReadIncomingMessageResult = {
29
30
  incomingMessage: undefined;
30
31
  end: false | true;
31
32
  };
32
- declare const createFuseLowLevel: ({ fuseFd, maxMessageSize }: {
33
+ declare const createFuseLowLevel: ({ syscalls, fuseFd, maxMessageSize }: {
34
+ syscalls: Pick<TSyscalls, "read" | "write">;
33
35
  fuseFd: number;
34
36
  maxMessageSize: number;
35
37
  }) => {
@@ -1,4 +1,4 @@
1
- import { read, write } from "./syscalls.js";
1
+ /*import type { TSyscalls } from "./syscalls.ts";*/
2
2
 
3
3
  /*type TIncomingRequest = {
4
4
  header: {
@@ -35,13 +35,16 @@ import { read, write } from "./syscalls.js";
35
35
  };*/
36
36
 
37
37
  const EPERM = 1;
38
+ const ENOENT = 2;
38
39
  const EAGAIN = 11;
39
40
  const ENODEV = 19;
40
41
 
41
42
  const createFuseLowLevel = ({
43
+ syscalls,
42
44
  fuseFd,
43
45
  maxMessageSize
44
46
  }/*: {
47
+ syscalls: Pick<TSyscalls, "read" | "write">;
45
48
  fuseFd: number;
46
49
  maxMessageSize: number;
47
50
  }*/) => {
@@ -51,7 +54,7 @@ const createFuseLowLevel = ({
51
54
  // eslint-disable-next-line complexity
52
55
  const tryReadIncomingMessage = ()/*: TTryReadIncomingMessageResult*/ => {
53
56
 
54
- const { errno, ret } = read({
57
+ const { errno, ret } = syscalls.read({
55
58
  fd: fuseFd,
56
59
  buffer: incomingBuffer
57
60
  });
@@ -89,7 +92,11 @@ const createFuseLowLevel = ({
89
92
  };
90
93
  }
91
94
 
92
- throw Error(`errno ${errno} not implemented yet`);
95
+ return {
96
+ error: Error(`read syscall failed with errno ${errno}`),
97
+ incomingMessage: undefined,
98
+ end: undefined
99
+ };
93
100
  }
94
101
 
95
102
  const incomingMessage = incomingBuffer.slice(0, Number(ret));
@@ -103,11 +110,17 @@ const createFuseLowLevel = ({
103
110
 
104
111
  const writeOutgoingMessage = ({ rawMessage }/*: { rawMessage: Uint8Array }*/) => {
105
112
 
106
- const { errno, ret } = write({
113
+ const { errno, ret } = syscalls.write({
107
114
  fd: fuseFd,
108
115
  buffer: rawMessage
109
116
  });
110
117
 
118
+ if (errno === ENOENT) {
119
+ // the kernel no longer waits for this reply, e.g. because the request was
120
+ // interrupted or the filesystem got unmounted while it was processed
121
+ return;
122
+ }
123
+
111
124
  if (errno !== undefined) {
112
125
  throw Error(`write syscall failed with errno ${errno}`);
113
126
  }
@@ -1,41 +1,11 @@
1
- import { type TRequestHandler } from "./server.js";
2
- import { fchdir } from "./syscalls.js";
3
- import { openFuseFd, defaultFusePath } from "./linux.js";
4
1
  import { createConvenienceRequestHandler } from "./convenience.js";
5
- import { moveMount, unmount, type TMountAttributes, type TRequiredFuseMountOptions } from "./mount.js";
2
+ import { defaultFusePath } from "./linux.js";
3
+ import type { TMountAttributes, TRequiredFuseMountOptions } from "./mount.js";
6
4
  import type { TFuseResponse } from "./protocol.js";
7
5
  import type { TFuseServerInterface } from "./convenience.js";
8
6
  import type { TDirent } from "./dirent.js";
9
7
  import { direntsToBuffer } from "./dirent.js";
10
- declare const createFuseFileSystem: ({ fuseFd, requestHandler, sniff, }: {
11
- fuseFd: number;
12
- requestHandler: TRequestHandler;
13
- sniff?: {
14
- rawMessageFromKernel: (args: {
15
- data: Uint8Array;
16
- }) => void;
17
- rawMessageToKernel: (args: {
18
- data: Uint8Array;
19
- }) => void;
20
- };
21
- }) => {
22
- mountDetached: ({ otherOptions, requiredOptions, mountAttributes, }: {
23
- otherOptions: {
24
- default_permissions?: boolean;
25
- allow_other?: boolean;
26
- allow_root?: boolean;
27
- } | Record<string, string | boolean>;
28
- requiredOptions: TRequiredFuseMountOptions;
29
- mountAttributes: TMountAttributes;
30
- }) => {
31
- error: Error;
32
- mountFd: undefined;
33
- } | {
34
- error: undefined;
35
- mountFd: number;
36
- };
37
- close: () => void;
38
- };
8
+ declare const createFuseFileSystem: any, openFuseFd: any, moveMount: any, unmount: any, fchdir: any;
39
9
  declare const createDefaultInitResult: () => Extract<TFuseResponse, {
40
10
  forOpcode: "INIT";
41
11
  errorCode: undefined;