Skip to content

Commit 32bd13e

Browse files
committed
Fixed compiler crashes when a contract name exceeds filesystem limits
1 parent a41a598 commit 32bd13e

File tree

6 files changed

+177
-1
lines changed

6 files changed

+177
-1
lines changed

dev-docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- [fix] Disallow self-inheritance for contracts and traits: PR [#3094](https://github.com/tact-lang/tact/pull/3094)
1313
- [fix] Added fixed-bytes support to bounced message size calculations: PR [#3129](https://github.com/tact-lang/tact/pull/3129)
14+
- [fix] Fixed compiler crashes when a contract name exceeds filesystem limits: PR [#3219](https://github.com/tact-lang/tact/pull/3219)
1415

1516
### Docs
1617

src/vfs/createNodeFileSystem.spec.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import path from "path";
22
import fs from "fs";
33
import { createNodeFileSystem } from "@/vfs/createNodeFileSystem";
4+
import { makeSafeName } from "@/vfs/utils";
45

56
describe("createNodeFileSystem", () => {
67
it("should open file system", () => {
@@ -46,4 +47,72 @@ describe("createNodeFileSystem", () => {
4647
fs.rmSync(realPathDir2, { recursive: true, force: true });
4748
}
4849
});
50+
51+
it("should truncate and hash long filenames", () => {
52+
const baseDir = path.resolve(__dirname, "./__testdata");
53+
const vfs = createNodeFileSystem(baseDir, false);
54+
55+
const longName = "A".repeat(300);
56+
const content = "Test content";
57+
const ext = ".md";
58+
59+
const inputPath = vfs.resolve(`${longName}${ext}`);
60+
const dir = path.dirname(inputPath);
61+
const expectedSafeName = makeSafeName(longName, ext);
62+
const expectedFullPath = path.join(dir, expectedSafeName);
63+
64+
try {
65+
if (fs.existsSync(expectedFullPath)) {
66+
fs.unlinkSync(expectedFullPath);
67+
}
68+
69+
vfs.writeFile(inputPath, content);
70+
expect(fs.existsSync(expectedFullPath)).toBe(true);
71+
72+
const actualContent = fs.readFileSync(expectedFullPath, "utf8");
73+
expect(actualContent).toBe(content);
74+
75+
expect(expectedSafeName.length).toBeLessThanOrEqual(255);
76+
expect(expectedSafeName).toMatch(
77+
new RegExp(
78+
`^${longName.slice(0, 255 - ext.length - 9)}_[0-9a-f]{8}${ext}$`,
79+
),
80+
);
81+
} finally {
82+
if (fs.existsSync(expectedFullPath)) {
83+
fs.unlinkSync(expectedFullPath);
84+
}
85+
}
86+
});
87+
it("should not truncate or hash short filenames", () => {
88+
const baseDir = path.resolve(__dirname, "./__testdata");
89+
const vfs = createNodeFileSystem(baseDir, false);
90+
91+
const shortName = "short-filename";
92+
const content = "Test content";
93+
const ext = ".md";
94+
95+
const inputPath = vfs.resolve(`${shortName}${ext}`);
96+
const dir = path.dirname(inputPath);
97+
const expectedSafeName = makeSafeName(shortName, ext);
98+
const expectedFullPath = path.join(dir, expectedSafeName);
99+
100+
try {
101+
if (fs.existsSync(expectedFullPath)) {
102+
fs.unlinkSync(expectedFullPath);
103+
}
104+
105+
vfs.writeFile(inputPath, content);
106+
expect(fs.existsSync(expectedFullPath)).toBe(true);
107+
108+
const actualContent = fs.readFileSync(expectedFullPath, "utf8");
109+
expect(actualContent).toBe(content);
110+
111+
expect(expectedSafeName).toBe(`${shortName}${ext}`);
112+
} finally {
113+
if (fs.existsSync(expectedFullPath)) {
114+
fs.unlinkSync(expectedFullPath);
115+
}
116+
}
117+
});
49118
});

src/vfs/createNodeFileSystem.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { VirtualFileSystem } from "@/vfs/VirtualFileSystem";
22
import fs from "fs";
33
import path from "path";
4+
import { getFullExtension, makeSafeName } from "@/vfs/utils";
45

56
function ensureInsideProjectRoot(filePath: string, root: string): void {
67
if (!filePath.startsWith(root)) {
@@ -47,6 +48,12 @@ export function createNodeFileSystem(
4748
if (readonly) {
4849
throw new Error("File system is readonly");
4950
}
51+
52+
const ext = getFullExtension(filePath);
53+
const name = path.basename(filePath, ext);
54+
const safeBase = makeSafeName(name, ext);
55+
filePath = path.join(path.dirname(filePath), safeBase);
56+
5057
ensureInsideProjectRoot(filePath, normalizedRoot);
5158
if (fs.existsSync(filePath)) {
5259
ensureNotSymlink(filePath);

src/vfs/createVirtualFileSystem.spec.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,54 @@ describe("createVirtualFileSystem", () => {
2727
expect(vfs.exists(realPath)).toBe(true);
2828
expect(vfs.readFile(realPath).toString()).toBe("");
2929
});
30+
31+
it("should truncate and hash long filenames", () => {
32+
const fs: Record<string, string> = {};
33+
const vfs = createVirtualFileSystem("@vroot", fs, false);
34+
35+
const longName = "A".repeat(300);
36+
const content = "Test content";
37+
const ext = ".md";
38+
39+
const inputPath = vfs.resolve("./", `${longName}${ext}`);
40+
41+
vfs.writeFile(inputPath, content);
42+
43+
const storedPaths = Object.keys(fs);
44+
expect(storedPaths.length).toBe(1);
45+
46+
const storedPath = storedPaths[0]!;
47+
expect(storedPath).toBeDefined();
48+
expect(storedPath.length).toBeLessThanOrEqual(255);
49+
50+
const regex = new RegExp(
51+
`^${longName.slice(0, 255 - ext.length - 9)}_[0-9a-f]{8}${ext}$`,
52+
);
53+
expect(storedPath).toMatch(regex);
54+
55+
expect(fs[storedPath]).toBe(Buffer.from(content).toString("base64"));
56+
});
57+
58+
it("should not truncate or hash short filenames", () => {
59+
const fs: Record<string, string> = {};
60+
const vfs = createVirtualFileSystem("@vroot", fs, false);
61+
62+
const shortName = "short-filename";
63+
const content = "Test content";
64+
const ext = ".md";
65+
66+
const inputPath = vfs.resolve("./", `${shortName}${ext}`);
67+
68+
vfs.writeFile(inputPath, content);
69+
70+
const storedPaths = Object.keys(fs);
71+
expect(storedPaths.length).toBe(1);
72+
73+
const storedPath = storedPaths[0]!;
74+
expect(storedPath).toBeDefined();
75+
76+
expect(storedPath).toBe(`${shortName}${ext}`);
77+
expect(fs[storedPath]).toBe(Buffer.from(content).toString("base64"));
78+
});
79+
3080
});

src/vfs/createVirtualFileSystem.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import normalize from "path-normalize";
22
import type { VirtualFileSystem } from "@/vfs/VirtualFileSystem";
3+
import path from "path";
4+
import { getFullExtension, makeSafeName } from "@/vfs/utils";
35

46
export function createVirtualFileSystem(
57
root: string,
@@ -47,7 +49,13 @@ export function createVirtualFileSystem(
4749
`Path '${filePath}' is outside of the root directory '${normalizedRoot}'`,
4850
);
4951
}
50-
const name = filePath.slice(normalizedRoot.length);
52+
const relPath = filePath.slice(normalizedRoot.length);
53+
const dir = path.dirname(relPath);
54+
const ext = getFullExtension(relPath);
55+
const base = path.basename(relPath, ext);
56+
57+
const safeName = makeSafeName(base, ext);
58+
const name = path.join(dir, safeName);
5159
fs[name] =
5260
typeof content === "string"
5361
? Buffer.from(content).toString("base64")

src/vfs/utils.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { sha256_sync } from "@ton/crypto";
2+
import path from "path";
3+
4+
/**
5+
* Ensures the resulting file name does not exceed the given maximum length.
6+
* If too long, trims the name and appends a short hash to avoid collisions.
7+
*
8+
* @param name - The base file name without extension.
9+
* @param ext - The file extension.
10+
* @param maxLen - Maximum allowed length for the full file name (default: 255).
11+
* @returns A safe file name within the specified length.
12+
*/
13+
export const makeSafeName = (
14+
name: string,
15+
ext: string,
16+
maxLen = 255,
17+
): string => {
18+
const full = name + ext;
19+
20+
if (full.length <= maxLen) {
21+
return full;
22+
}
23+
24+
const hash = sha256_sync(Buffer.from(name)).toString("hex").slice(0, 8);
25+
const suffix = `_${hash}${ext}`;
26+
const maxNameLen = maxLen - suffix.length;
27+
const safeName = name.slice(0, maxNameLen);
28+
29+
return `${safeName}${suffix}`;
30+
};
31+
32+
/**
33+
* Returns the full extension of a file, including all parts after the first dot.
34+
* - "file.txt" => ".txt"
35+
* - "archive.tar.gz" => ".tar.gz"
36+
*/
37+
export const getFullExtension = (filename: string): string => {
38+
const base = path.basename(filename);
39+
const firstDotIndex = base.indexOf(".");
40+
return firstDotIndex !== -1 ? base.slice(firstDotIndex) : "";
41+
};

0 commit comments

Comments
 (0)