Skip to content

Commit e79af3a

Browse files
committed
fix(lib-storage): restrict lstat calls to fs.ReadStream objects
1 parent bc9d888 commit e79af3a

File tree

6 files changed

+134
-53
lines changed

6 files changed

+134
-53
lines changed
Lines changed: 57 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,78 @@
1-
import { describe, expect, it, vi } from "vitest";
1+
import fs from "node:fs";
2+
import { describe, expect, it } from "vitest";
23

3-
import { BYTE_LENGTH_SOURCE, byteLengthSource } from "./byteLengthSource";
4-
import { runtimeConfig } from "./runtimeConfig";
4+
import { byteLength } from "./byteLength";
55

6-
vi.mock("./runtimeConfig", () => ({
7-
runtimeConfig: {
8-
lstatSync: vi.fn(),
9-
},
10-
}));
11-
12-
describe("byteLengthSource", () => {
13-
it("should return CONTENT_LENGTH when override is provided", () => {
14-
expect(byteLengthSource({}, 100)).toBe(BYTE_LENGTH_SOURCE.CONTENT_LENGTH);
6+
describe("byteLength", () => {
7+
it("should handle null and undefined input", () => {
8+
expect(byteLength(null)).toBe(0);
9+
expect(byteLength(undefined)).toBe(0);
1510
});
1611

17-
it("should return EMPTY_INPUT for null input", () => {
18-
expect(byteLengthSource(null)).toBe(BYTE_LENGTH_SOURCE.EMPTY_INPUT);
19-
});
12+
describe("strings", () => {
13+
it("empty", () => {
14+
expect(byteLength("")).toBe(0);
15+
});
2016

21-
it("should return EMPTY_INPUT for undefined input", () => {
22-
expect(byteLengthSource(undefined)).toBe(BYTE_LENGTH_SOURCE.EMPTY_INPUT);
23-
});
17+
it("should return correct length for ASCII characters", () => {
18+
expect(byteLength("hello")).toBe(5);
19+
expect(byteLength("12345")).toBe(5);
20+
expect(byteLength("!@#$%")).toBe(5);
21+
});
2422

25-
it("should return STRING_LENGTH for string input", () => {
26-
expect(byteLengthSource("test")).toBe(BYTE_LENGTH_SOURCE.STRING_LENGTH);
27-
});
23+
it("should return correct length for unicode characters", () => {
24+
expect(byteLength("😀")).toBe(4);
25+
});
2826

29-
it("should return TYPED_ARRAY for input with byteLength", () => {
30-
const input = new Uint8Array(10);
31-
expect(byteLengthSource(input)).toBe(BYTE_LENGTH_SOURCE.TYPED_ARRAY);
27+
it("should handle mixed ASCII and unicode characters", () => {
28+
expect(byteLength("hello 世界")).toBe(12);
29+
expect(byteLength("hi 😀")).toBe(7);
30+
});
3231
});
3332

34-
it("should return LENGTH for input with length property", () => {
35-
const input = { length: 10 };
36-
expect(byteLengthSource(input)).toBe(BYTE_LENGTH_SOURCE.LENGTH);
37-
});
33+
describe("byte arrays", () => {
34+
it("should handle Uint8Array", () => {
35+
expect(byteLength(new Uint8Array([1, 2, 3]))).toBe(3);
36+
expect(byteLength(new Uint8Array([]))).toBe(0);
37+
});
3838

39-
it("should return SIZE for input with size property", () => {
40-
const input = { size: 10 };
41-
expect(byteLengthSource(input)).toBe(BYTE_LENGTH_SOURCE.SIZE);
39+
it("should handle Buffer", () => {
40+
expect(byteLength(Buffer.from([1, 2, 3]))).toBe(3);
41+
expect(byteLength(Buffer.from([]))).toBe(0);
42+
});
4243
});
4344

44-
it("should return START_END_DIFF for input with start and end properties", () => {
45-
const input = { start: 0, end: 10 };
46-
expect(byteLengthSource(input)).toBe(BYTE_LENGTH_SOURCE.START_END_DIFF);
47-
});
45+
describe("things with length or size properties", () => {
46+
it("should handle arrays", () => {
47+
expect(byteLength([1, 2, 3])).toBe(3);
48+
expect(byteLength([])).toBe(0);
49+
});
4850

49-
it("should return LSTAT for input with path that exists", () => {
50-
const input = { path: "/test/path" };
51-
vi.mocked(runtimeConfig.lstatSync).mockReturnValue({ size: 100 } as any);
51+
it("should handle objects with length property", () => {
52+
expect(byteLength({ length: 5 })).toBe(5);
53+
expect(byteLength({ length: 0 })).toBe(0);
54+
});
5255

53-
expect(byteLengthSource(input)).toBe(BYTE_LENGTH_SOURCE.LSTAT);
54-
expect(runtimeConfig.lstatSync).toHaveBeenCalledWith("/test/path");
56+
it("should handle objects with size property", () => {
57+
expect(byteLength({ size: 10 })).toBe(10);
58+
expect(byteLength({ size: 0 })).toBe(0);
59+
});
5560
});
5661

57-
it("should return undefined for input with path that throws error", () => {
58-
const input = { path: "/test/path" };
59-
vi.mocked(runtimeConfig.lstatSync).mockImplementation(() => {
60-
throw new Error("File not found");
62+
describe("start end differentials", () => {
63+
it("should handle readable streams", () => {
64+
const stream = fs.createReadStream(__filename, {
65+
start: 1000,
66+
end: 1499,
67+
});
68+
expect(byteLength(stream)).toBe(500);
6169
});
62-
63-
expect(byteLengthSource(input)).toBeUndefined();
6470
});
6571

66-
it("should return undefined for input with no matching properties", () => {
67-
const input = { foo: "bar" };
68-
expect(byteLengthSource(input)).toBeUndefined();
72+
describe("filestreams", () => {
73+
it("should handle readable streams", () => {
74+
const stream = fs.createReadStream(__filename);
75+
expect(byteLength(stream)).toBe(fs.lstatSync(__filename).size);
76+
});
6977
});
7078
});

lib/lib-storage/src/byteLength.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ export const byteLength = (input: any): number | undefined => {
2929
} else if (typeof input.start === "number" && typeof input.end === "number") {
3030
// file read stream with range.
3131
return input.end + 1 - input.start;
32-
} else if (typeof input.path === "string") {
33-
// file read stream with path.
32+
} else if (runtimeConfig.isFileReadStream(input)) {
3433
try {
3534
return runtimeConfig.lstatSync(input.path).size;
3635
} catch (error) {
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import fs from "node:fs";
2+
import { describe, expect, it, vi } from "vitest";
3+
4+
import { BYTE_LENGTH_SOURCE, byteLengthSource } from "./byteLengthSource";
5+
import { runtimeConfig } from "./runtimeConfig";
6+
7+
describe("byteLengthSource", () => {
8+
it("should return CONTENT_LENGTH when override is provided", () => {
9+
expect(byteLengthSource({}, 100)).toBe(BYTE_LENGTH_SOURCE.CONTENT_LENGTH);
10+
});
11+
12+
it("should return EMPTY_INPUT for null input", () => {
13+
expect(byteLengthSource(null)).toBe(BYTE_LENGTH_SOURCE.EMPTY_INPUT);
14+
});
15+
16+
it("should return EMPTY_INPUT for undefined input", () => {
17+
expect(byteLengthSource(undefined)).toBe(BYTE_LENGTH_SOURCE.EMPTY_INPUT);
18+
});
19+
20+
it("should return STRING_LENGTH for string input", () => {
21+
expect(byteLengthSource("test")).toBe(BYTE_LENGTH_SOURCE.STRING_LENGTH);
22+
});
23+
24+
it("should return TYPED_ARRAY for input with byteLength", () => {
25+
const input = new Uint8Array(10);
26+
expect(byteLengthSource(input)).toBe(BYTE_LENGTH_SOURCE.TYPED_ARRAY);
27+
});
28+
29+
it("should return LENGTH for input with length property", () => {
30+
const input = { length: 10 };
31+
expect(byteLengthSource(input)).toBe(BYTE_LENGTH_SOURCE.LENGTH);
32+
});
33+
34+
it("should return SIZE for input with size property", () => {
35+
const input = { size: 10 };
36+
expect(byteLengthSource(input)).toBe(BYTE_LENGTH_SOURCE.SIZE);
37+
});
38+
39+
it("should return START_END_DIFF for input with start and end properties", () => {
40+
const input = { start: 0, end: 10 };
41+
expect(byteLengthSource(input)).toBe(BYTE_LENGTH_SOURCE.START_END_DIFF);
42+
});
43+
44+
it("should return LSTAT for input with path that exists", () => {
45+
const input = fs.createReadStream(__filename);
46+
vi.spyOn(runtimeConfig, "lstatSync");
47+
48+
expect(byteLengthSource(input)).toBe(BYTE_LENGTH_SOURCE.LSTAT);
49+
expect(runtimeConfig.lstatSync).toHaveBeenCalledWith(__filename);
50+
});
51+
52+
it("ignores objects with a path property that aren't fs.ReadStream objects", () => {
53+
const input = { path: __filename };
54+
55+
expect(byteLengthSource(input)).toBe(undefined);
56+
});
57+
58+
it("should return undefined for input with path that throws error", () => {
59+
const input = { path: "surely-this-path-does-not-exist" };
60+
61+
expect(byteLengthSource(input)).toBeUndefined();
62+
});
63+
64+
it("should return undefined for input with no matching properties", () => {
65+
const input = { foo: "bar" };
66+
expect(byteLengthSource(input)).toBeUndefined();
67+
});
68+
});

lib/lib-storage/src/byteLengthSource.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export const byteLengthSource = (input: any, override?: number): BYTE_LENGTH_SOU
4242
return BYTE_LENGTH_SOURCE.SIZE;
4343
} else if (typeof input.start === "number" && typeof input.end === "number") {
4444
return BYTE_LENGTH_SOURCE.START_END_DIFF;
45-
} else if (typeof input.path === "string") {
45+
} else if (runtimeConfig.isFileReadStream(input)) {
4646
try {
4747
runtimeConfig.lstatSync(input.path).size;
4848
return BYTE_LENGTH_SOURCE.LSTAT;

lib/lib-storage/src/runtimeConfig.shared.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,7 @@
33
*/
44
export const runtimeConfigShared = {
55
lstatSync: () => {},
6+
isFileReadStream(f: unknown) {
7+
return false;
8+
},
69
};

lib/lib-storage/src/runtimeConfig.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { lstatSync } from "fs";
1+
import { lstatSync, ReadStream } from "fs";
22

33
import { runtimeConfigShared as shared } from "./runtimeConfig.shared";
44

@@ -9,4 +9,7 @@ export const runtimeConfig = {
99
...shared,
1010
runtime: "node",
1111
lstatSync,
12+
isFileReadStream(f: unknown): f is ReadStream {
13+
return f instanceof ReadStream;
14+
},
1215
};

0 commit comments

Comments
 (0)