@k13engineering/linux-fuse 0.0.1 → 0.0.3

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,133 @@
1
+ import { read, write } from "./syscalls.js";
2
+
3
+ /*type TIncomingRequest = {
4
+ header: {
5
+ opcode: bigint;
6
+ unique: bigint;
7
+ nodeid: bigint;
8
+ uid: bigint;
9
+ gid: bigint;
10
+ pid: bigint;
11
+ },
12
+ payload: Uint8Array
13
+ };*/
14
+
15
+ /*type TOutgoingResponse = {
16
+ header: {
17
+ error: bigint;
18
+ unique: bigint;
19
+ },
20
+ payload: Uint8Array
21
+ };*/
22
+
23
+ /*type TTryReadIncomingMessageResult = {
24
+ error: Error;
25
+ incomingMessage: undefined;
26
+ end: undefined;
27
+ } | {
28
+ error: undefined;
29
+ incomingMessage: Uint8Array;
30
+ end: false;
31
+ } | {
32
+ error: undefined;
33
+ incomingMessage: undefined;
34
+ end: false | true;
35
+ };*/
36
+
37
+ const EPERM = 1;
38
+ const EAGAIN = 11;
39
+ const ENODEV = 19;
40
+
41
+ const createFuseLowLevel = ({
42
+ fuseFd,
43
+ maxMessageSize
44
+ }/*: {
45
+ fuseFd: number;
46
+ maxMessageSize: number;
47
+ }*/) => {
48
+
49
+ const incomingBuffer = new Uint8Array(maxMessageSize);
50
+
51
+ // eslint-disable-next-line complexity
52
+ const tryReadIncomingMessage = ()/*: TTryReadIncomingMessageResult*/ => {
53
+
54
+ const { errno, ret } = read({
55
+ fd: fuseFd,
56
+ buffer: incomingBuffer
57
+ });
58
+
59
+ if (errno !== undefined) {
60
+
61
+ if (errno === EPERM) {
62
+
63
+ // most likely the filesystem hasn't been mounted yet
64
+
65
+ return {
66
+ error: undefined,
67
+ incomingMessage: undefined,
68
+ end: false
69
+ };
70
+ }
71
+
72
+ if (errno === EAGAIN) {
73
+ // no message to read
74
+
75
+ return {
76
+ error: undefined,
77
+ incomingMessage: undefined,
78
+ end: false
79
+ };
80
+ }
81
+
82
+ if (errno === ENODEV) {
83
+ // the filesystem has been unmounted
84
+
85
+ return {
86
+ error: undefined,
87
+ incomingMessage: undefined,
88
+ end: true
89
+ };
90
+ }
91
+
92
+ throw Error(`errno ${errno} not implemented yet`);
93
+ }
94
+
95
+ const incomingMessage = incomingBuffer.slice(0, Number(ret));
96
+
97
+ return {
98
+ error: undefined,
99
+ incomingMessage,
100
+ end: false
101
+ };
102
+ };
103
+
104
+ const writeOutgoingMessage = ({ rawMessage }/*: { rawMessage: Uint8Array }*/) => {
105
+
106
+ const { errno, ret } = write({
107
+ fd: fuseFd,
108
+ buffer: rawMessage
109
+ });
110
+
111
+ if (errno !== undefined) {
112
+ throw Error(`write syscall failed with errno ${errno}`);
113
+ }
114
+
115
+ if (ret !== BigInt(rawMessage.length)) {
116
+ throw Error(`short write not supported yet`);
117
+ }
118
+ };
119
+
120
+ return {
121
+ tryReadIncomingMessage,
122
+ writeOutgoingMessage,
123
+ };
124
+ };
125
+
126
+ export {
127
+ createFuseLowLevel
128
+ };
129
+
130
+ /*export type {
131
+ TIncomingRequest,
132
+ TOutgoingResponse
133
+ };*/
@@ -0,0 +1,43 @@
1
+ import { type TRequestHandler } from "./server.js";
2
+ import { openFuseFd, defaultFusePath } from "./linux.js";
3
+ import { createConvenienceRequestHandler } from "./convenience.js";
4
+ import { moveMount, unmount, type TMountAttributes, type TRequiredFuseMountOptions } from "./mount.js";
5
+ import type { TFuseResponse } from "./protocol.js";
6
+ import type { TFuseServerInterface } from "./convenience.js";
7
+ import type { TDirent } from "./dirent.js";
8
+ import { direntsToBuffer } from "./dirent.js";
9
+ declare const createFuseFileSystem: ({ fuseFd, requestHandler, sniff, }: {
10
+ fuseFd: number;
11
+ requestHandler: TRequestHandler;
12
+ sniff?: {
13
+ rawMessageFromKernel: (args: {
14
+ data: Uint8Array;
15
+ }) => void;
16
+ rawMessageToKernel: (args: {
17
+ data: Uint8Array;
18
+ }) => void;
19
+ };
20
+ }) => {
21
+ mountDetached: ({ otherOptions, requiredOptions, mountAttributes, }: {
22
+ otherOptions: {
23
+ default_permissions?: boolean;
24
+ allow_other?: boolean;
25
+ allow_root?: boolean;
26
+ } | Record<string, string | boolean>;
27
+ requiredOptions: TRequiredFuseMountOptions;
28
+ mountAttributes: TMountAttributes;
29
+ }) => {
30
+ error: Error;
31
+ mountFd: undefined;
32
+ } | {
33
+ error: undefined;
34
+ mountFd: number;
35
+ };
36
+ close: () => void;
37
+ };
38
+ declare const createDefaultInitResult: () => Extract<TFuseResponse, {
39
+ forOpcode: "INIT";
40
+ errorCode: undefined;
41
+ }>["result"];
42
+ export { createFuseFileSystem, openFuseFd, defaultFusePath, createConvenienceRequestHandler, moveMount, unmount, createDefaultInitResult, direntsToBuffer };
43
+ export type { TRequiredFuseMountOptions, TMountAttributes, TFuseServerInterface, TDirent, };
@@ -0,0 +1,325 @@
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
+ } from "./constants.js";
10
+ import nodeFs from "node:fs";
11
+ import { fsconfig, fsmount, fsopen } from "./syscalls.js";
12
+ import { openFuseFd, defaultFusePath } from "./linux.js";
13
+ import { createConvenienceRequestHandler } from "./convenience.js";
14
+ import {
15
+ mountAttributesToBitmask,
16
+ moveMount,
17
+ unmount,
18
+ /*type TMountAttributes,
19
+ *//*type TRequiredFuseMountOptions
20
+ */} from "./mount.js";
21
+ import { createPoller } from "@k13engineering/uv-poll";
22
+ /*import type { TFuseResponse } from "./protocol.ts";*/
23
+ /*import type { TFuseServerInterface } from "./convenience.ts";*/
24
+ /*import type { TDirent } from "./dirent.ts";*/
25
+ import { direntsToBuffer } from "./dirent.js";
26
+
27
+ const UV_EBADF = -9;
28
+
29
+ const MAX_ALLOWED_WRITE = 32n * 1024n * 1024n;
30
+
31
+ const createFuseFileSystem = ({
32
+ fuseFd,
33
+ requestHandler,
34
+ sniff,
35
+ }/*: {
36
+ fuseFd: number;
37
+ requestHandler: TRequestHandler;
38
+
39
+ sniff?: {
40
+ rawMessageFromKernel: (args: { data: Uint8Array }) => void;
41
+ rawMessageToKernel: (args: { data: Uint8Array }) => void;
42
+ }
43
+ }*/) => {
44
+
45
+ let lowLevel = createFuseLowLevel({
46
+ fuseFd,
47
+ maxMessageSize: 1 * 1024 * 1024 + 4096,
48
+ });
49
+
50
+ const server = createFuseServer({
51
+ requestHandler: {
52
+ // eslint-disable-next-line complexity
53
+ handleRequest: async ({ request }) => {
54
+ const response = await requestHandler.handleRequest({ request });
55
+
56
+ if (response !== undefined && response.forOpcode === "INIT" && response.errorCode === undefined) {
57
+
58
+ if (response.result.maxWrite >= MAX_ALLOWED_WRITE) {
59
+ throw Error(`maxWrite of ${response.result.maxWrite} is too large, refusing to operate`);
60
+ }
61
+
62
+ lowLevel = createFuseLowLevel({
63
+ fuseFd,
64
+ maxMessageSize: Number(response.result.maxWrite) + 4096,
65
+ });
66
+ }
67
+
68
+ return response;
69
+ }
70
+ },
71
+ sendMessageToKernel: ({ rawMessageToKernel }) => {
72
+ sniff?.rawMessageToKernel({ data: rawMessageToKernel });
73
+ lowLevel.writeOutgoingMessage({
74
+ rawMessage: rawMessageToKernel
75
+ });
76
+ }
77
+ });
78
+
79
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
80
+ let ended = false;
81
+ let closed = false;
82
+
83
+ const poller = createPoller({
84
+ fd: fuseFd
85
+ });
86
+
87
+ const handlePollError = ({ errorCode }/*: { errorCode: number }*/) => {
88
+ if (errorCode === UV_EBADF) {
89
+ // EBADF is raised when the kernel unmounts the filesystem
90
+ ended = true;
91
+ return;
92
+ }
93
+
94
+ throw Error(`poll error with code ${errorCode}`);
95
+ };
96
+
97
+ // eslint-disable-next-line complexity
98
+ const next = () => {
99
+
100
+ if (closed) {
101
+ throw Error("BUG: should not be called after close");
102
+ }
103
+
104
+ const { error: readError, incomingMessage, end } = lowLevel.tryReadIncomingMessage();
105
+ if (readError !== undefined) {
106
+ throw readError;
107
+ }
108
+
109
+ if (end) {
110
+ ended = true;
111
+ return;
112
+ }
113
+
114
+ if (incomingMessage !== undefined) {
115
+ sniff?.rawMessageFromKernel({ data: incomingMessage });
116
+ server.processRequest({
117
+ rawIncomingRequest: incomingMessage
118
+ });
119
+ }
120
+
121
+ poller.armOnce({
122
+ error: ({ errorCode }) => {
123
+ handlePollError({ errorCode });
124
+ },
125
+
126
+ readable: () => {
127
+ next();
128
+ }
129
+ });
130
+ };
131
+
132
+ poller.armOnce({
133
+ error: ({ errorCode }) => {
134
+ handlePollError({ errorCode });
135
+ },
136
+
137
+ readable: () => {
138
+ next();
139
+ }
140
+ });
141
+
142
+ /*type TFsMountResult = {
143
+ error: Error;
144
+ mountFd: undefined;
145
+ } | {
146
+ error: undefined;
147
+ mountFd: number;
148
+ };*/
149
+
150
+ // eslint-disable-next-line max-statements
151
+ const mountDetached = ({
152
+ otherOptions,
153
+ requiredOptions,
154
+ mountAttributes,
155
+ }/*: {
156
+ otherOptions: {
157
+ default_permissions?: boolean;
158
+ allow_other?: boolean;
159
+ allow_root?: boolean;
160
+ } | Record<string, string | boolean>;
161
+ requiredOptions: TRequiredFuseMountOptions;
162
+ mountAttributes: TMountAttributes;
163
+ }*/)/*: TFsMountResult*/ => {
164
+
165
+ const { errno: fsopenErrno, fsfd } = fsopen({
166
+ fileSystemName: "fuse",
167
+ flags: FSOPEN_CLOEXEC
168
+ });
169
+
170
+ if (fsopenErrno !== undefined) {
171
+ return {
172
+ error: Error(`fsopen syscall failed with errno ${fsopenErrno}`),
173
+ mountFd: undefined,
174
+ };
175
+ }
176
+
177
+ const configureString = ({ key, value }/*: { key: string; value: string }*/) => {
178
+ const { errno } = fsconfig({
179
+ fd: fsfd,
180
+ command: FSCONFIG_SET_STRING,
181
+ key,
182
+ value,
183
+ auxiliaryValue: 0,
184
+ });
185
+
186
+ if (errno !== undefined) {
187
+ throw Error(`fsconfig syscall failed with errno ${errno} when setting ${key}=${value}`);
188
+ }
189
+ };
190
+
191
+ const configureFlag = ({ key, value }/*: { key: string; value: boolean }*/) => {
192
+ if (!value) {
193
+ return;
194
+ }
195
+
196
+ const { errno } = fsconfig({
197
+ fd: fsfd,
198
+ command: FSCONFIG_SET_FLAG,
199
+ key,
200
+ value: undefined,
201
+ auxiliaryValue: 0,
202
+ });
203
+
204
+ if (errno !== undefined) {
205
+ throw Error(`fsconfig syscall failed with errno ${errno} when setting flag ${key}`);
206
+ }
207
+ };
208
+
209
+ const forbiddenOptions = ["fd", "rootmode", "user_id", "group_id"];
210
+
211
+ Object.keys(otherOptions).forEach((key) => {
212
+ const value = otherOptions[key /*as keyof typeof otherOptions*/];
213
+
214
+ if (forbiddenOptions.includes(key)) {
215
+ throw Error(`option ${key} is not allowed as it is required for correct mounting`);
216
+ }
217
+
218
+ if (value === undefined) {
219
+ return;
220
+ }
221
+
222
+ if (typeof value === "boolean") {
223
+ configureFlag({ key, value });
224
+ } else {
225
+ configureString({ key, value });
226
+ }
227
+ });
228
+
229
+ configureString({ key: "fd", value: `${fuseFd}` });
230
+ configureString({ key: "rootmode", value: `${requiredOptions.rootmode.toString(8)}` });
231
+ configureString({ key: "user_id", value: `${requiredOptions.user_id}` });
232
+ configureString({ key: "group_id", value: `${requiredOptions.group_id}` });
233
+
234
+ const { errno: createErrno } = fsconfig({
235
+ fd: fsfd,
236
+ command: FSCONFIG_CMD_CREATE,
237
+ key: undefined,
238
+ value: undefined,
239
+ auxiliaryValue: 0,
240
+ });
241
+ if (createErrno !== undefined) {
242
+
243
+ nodeFs.closeSync(fsfd);
244
+
245
+ return {
246
+ error: Error(`fsconfig CMD_CREATE syscall failed with errno ${createErrno}`),
247
+ mountFd: undefined,
248
+ };
249
+ }
250
+
251
+ const { errno: mountErrno, mountFd } = fsmount({
252
+ fd: fsfd,
253
+ flags: FSMOUNT_CLOEXEC,
254
+ mountAttributesBitmask: mountAttributesToBitmask({
255
+ mountAttributes
256
+ })
257
+ });
258
+
259
+ nodeFs.closeSync(fsfd);
260
+
261
+ if (mountErrno !== undefined) {
262
+ return {
263
+ error: Error(`fsmount syscall failed with errno ${mountErrno}`),
264
+ mountFd: undefined,
265
+ };
266
+ }
267
+
268
+ return {
269
+ error: undefined,
270
+ mountFd,
271
+ };
272
+ };
273
+
274
+ const close = () => {
275
+ if (closed) {
276
+ throw Error("already closed");
277
+ }
278
+
279
+ closed = true;
280
+
281
+ poller.close();
282
+ nodeFs.closeSync(fuseFd);
283
+ };
284
+
285
+ return {
286
+ mountDetached,
287
+ close,
288
+ };
289
+ };
290
+
291
+ const createDefaultInitResult = ()/*: Extract<TFuseResponse, { forOpcode: "INIT", errorCode: undefined }>["result"]*/ => {
292
+ return {
293
+ major: 7n,
294
+ minor: 45n,
295
+ maxReadahead: 4096n,
296
+ flags: 0n,
297
+ maxBackground: 0n,
298
+ congestionThreshold: 0n,
299
+ maxWrite: 1n * 1024n * 1024n,
300
+ timeGran: 1n,
301
+ maxPages: 0n,
302
+ mapAlignment: 0n,
303
+ flags2: 0n,
304
+ maxStackDepth: 0n,
305
+ requestTimeout: 0n,
306
+ };
307
+ };
308
+
309
+ export {
310
+ createFuseFileSystem,
311
+ openFuseFd,
312
+ defaultFusePath,
313
+ createConvenienceRequestHandler,
314
+ moveMount,
315
+ unmount,
316
+ createDefaultInitResult,
317
+ direntsToBuffer
318
+ };
319
+
320
+ /*export type {
321
+ TRequiredFuseMountOptions,
322
+ TMountAttributes,
323
+ TFuseServerInterface,
324
+ TDirent,
325
+ };*/
@@ -0,0 +1,12 @@
1
+ type TOpenResult = {
2
+ error: Error;
3
+ fuseFd: undefined;
4
+ } | {
5
+ error: undefined;
6
+ fuseFd: number;
7
+ };
8
+ declare const defaultFusePath = "/dev/fuse";
9
+ declare const openFuseFd: ({ fusePath }: {
10
+ fusePath: string;
11
+ }) => TOpenResult;
12
+ export { defaultFusePath, openFuseFd };
@@ -0,0 +1,32 @@
1
+ import nodeFs from "node:fs";
2
+
3
+ /*type TOpenResult = {
4
+ error: Error;
5
+ fuseFd: undefined;
6
+ } | {
7
+ error: undefined;
8
+ fuseFd: number;
9
+ };*/
10
+
11
+ const defaultFusePath = "/dev/fuse";
12
+
13
+ const openFuseFd = ({ fusePath }/*: { fusePath: string }*/)/*: TOpenResult*/ => {
14
+ try {
15
+ const fuseFd = nodeFs.openSync(fusePath, nodeFs.constants.O_RDWR | nodeFs.constants.O_NONBLOCK);
16
+
17
+ return {
18
+ error: undefined,
19
+ fuseFd,
20
+ };
21
+ } catch (err) {
22
+ return {
23
+ error: Error(`failed to open "${defaultFusePath}": ${err}`),
24
+ fuseFd: undefined,
25
+ };
26
+ }
27
+ };
28
+
29
+ export {
30
+ defaultFusePath,
31
+ openFuseFd
32
+ };
@@ -0,0 +1,50 @@
1
+ type TParsedMessageFromKernel = {
2
+ header: {
3
+ opcode: bigint;
4
+ unique: bigint;
5
+ nodeid: bigint;
6
+ uid: bigint;
7
+ gid: bigint;
8
+ pid: bigint;
9
+ };
10
+ payload: Uint8Array;
11
+ };
12
+ type TParsedMessageToKernel = {
13
+ header: {
14
+ error: bigint;
15
+ unique: bigint;
16
+ };
17
+ payload: Uint8Array;
18
+ };
19
+ type TParseMessageFromKernelResult = {
20
+ error: Error;
21
+ parsedMessageFromKernel: undefined;
22
+ } | {
23
+ error: undefined;
24
+ parsedMessageFromKernel: TParsedMessageFromKernel;
25
+ };
26
+ type TParseMessageToKernelResult = {
27
+ error: Error;
28
+ parsedMessageToKernel: undefined;
29
+ } | {
30
+ error: undefined;
31
+ parsedMessageToKernel: TParsedMessageToKernel;
32
+ };
33
+ declare const createMessageFromKernelParser: () => {
34
+ parseMessageFromKernel: ({ data }: {
35
+ data: Uint8Array;
36
+ }) => TParseMessageFromKernelResult;
37
+ formatMessageFromKernel: ({ parsedMessageFromKernel }: {
38
+ parsedMessageFromKernel: TParsedMessageFromKernel;
39
+ }) => Uint8Array;
40
+ };
41
+ declare const createMessageToKernelParser: () => {
42
+ formatMessageToKernel: ({ parsedMessageToKernel }: {
43
+ parsedMessageToKernel: TParsedMessageToKernel;
44
+ }) => Uint8Array;
45
+ parseMessageToKernel: ({ data }: {
46
+ data: Uint8Array;
47
+ }) => TParseMessageToKernelResult;
48
+ };
49
+ export { createMessageFromKernelParser, createMessageToKernelParser };
50
+ export type { TParsedMessageFromKernel, TParsedMessageToKernel };