@k13engineering/linux-fuse 0.0.1 → 0.0.2

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