From 32bd13ec4635dcc8b2abb7dfca5334267537574d Mon Sep 17 00:00:00 2001 From: hazyone Date: Thu, 22 May 2025 22:26:29 +0200 Subject: [PATCH 1/2] Fixed compiler crashes when a contract name exceeds filesystem limits --- dev-docs/CHANGELOG.md | 1 + src/vfs/createNodeFileSystem.spec.ts | 69 +++++++++++++++++++++++++ src/vfs/createNodeFileSystem.ts | 7 +++ src/vfs/createVirtualFileSystem.spec.ts | 50 ++++++++++++++++++ src/vfs/createVirtualFileSystem.ts | 10 +++- src/vfs/utils.ts | 41 +++++++++++++++ 6 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 src/vfs/utils.ts diff --git a/dev-docs/CHANGELOG.md b/dev-docs/CHANGELOG.md index 7358294965..2ec11fed44 100644 --- a/dev-docs/CHANGELOG.md +++ b/dev-docs/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [fix] Disallow self-inheritance for contracts and traits: PR [#3094](https://github.com/tact-lang/tact/pull/3094) - [fix] Added fixed-bytes support to bounced message size calculations: PR [#3129](https://github.com/tact-lang/tact/pull/3129) +- [fix] Fixed compiler crashes when a contract name exceeds filesystem limits: PR [#3219](https://github.com/tact-lang/tact/pull/3219) ### Docs diff --git a/src/vfs/createNodeFileSystem.spec.ts b/src/vfs/createNodeFileSystem.spec.ts index bd2b35d154..20f0afa187 100644 --- a/src/vfs/createNodeFileSystem.spec.ts +++ b/src/vfs/createNodeFileSystem.spec.ts @@ -1,6 +1,7 @@ import path from "path"; import fs from "fs"; import { createNodeFileSystem } from "@/vfs/createNodeFileSystem"; +import { makeSafeName } from "@/vfs/utils"; describe("createNodeFileSystem", () => { it("should open file system", () => { @@ -46,4 +47,72 @@ describe("createNodeFileSystem", () => { fs.rmSync(realPathDir2, { recursive: true, force: true }); } }); + + it("should truncate and hash long filenames", () => { + const baseDir = path.resolve(__dirname, "./__testdata"); + const vfs = createNodeFileSystem(baseDir, false); + + const longName = "A".repeat(300); + const content = "Test content"; + const ext = ".md"; + + const inputPath = vfs.resolve(`${longName}${ext}`); + const dir = path.dirname(inputPath); + const expectedSafeName = makeSafeName(longName, ext); + const expectedFullPath = path.join(dir, expectedSafeName); + + try { + if (fs.existsSync(expectedFullPath)) { + fs.unlinkSync(expectedFullPath); + } + + vfs.writeFile(inputPath, content); + expect(fs.existsSync(expectedFullPath)).toBe(true); + + const actualContent = fs.readFileSync(expectedFullPath, "utf8"); + expect(actualContent).toBe(content); + + expect(expectedSafeName.length).toBeLessThanOrEqual(255); + expect(expectedSafeName).toMatch( + new RegExp( + `^${longName.slice(0, 255 - ext.length - 9)}_[0-9a-f]{8}${ext}$`, + ), + ); + } finally { + if (fs.existsSync(expectedFullPath)) { + fs.unlinkSync(expectedFullPath); + } + } + }); + it("should not truncate or hash short filenames", () => { + const baseDir = path.resolve(__dirname, "./__testdata"); + const vfs = createNodeFileSystem(baseDir, false); + + const shortName = "short-filename"; + const content = "Test content"; + const ext = ".md"; + + const inputPath = vfs.resolve(`${shortName}${ext}`); + const dir = path.dirname(inputPath); + const expectedSafeName = makeSafeName(shortName, ext); + const expectedFullPath = path.join(dir, expectedSafeName); + + try { + if (fs.existsSync(expectedFullPath)) { + fs.unlinkSync(expectedFullPath); + } + + vfs.writeFile(inputPath, content); + expect(fs.existsSync(expectedFullPath)).toBe(true); + + const actualContent = fs.readFileSync(expectedFullPath, "utf8"); + expect(actualContent).toBe(content); + + expect(expectedSafeName).toBe(`${shortName}${ext}`); + } finally { + if (fs.existsSync(expectedFullPath)) { + fs.unlinkSync(expectedFullPath); + } + } + }); }); diff --git a/src/vfs/createNodeFileSystem.ts b/src/vfs/createNodeFileSystem.ts index a032756d2a..c7d04c60c5 100644 --- a/src/vfs/createNodeFileSystem.ts +++ b/src/vfs/createNodeFileSystem.ts @@ -1,6 +1,7 @@ import type { VirtualFileSystem } from "@/vfs/VirtualFileSystem"; import fs from "fs"; import path from "path"; +import { getFullExtension, makeSafeName } from "@/vfs/utils"; function ensureInsideProjectRoot(filePath: string, root: string): void { if (!filePath.startsWith(root)) { @@ -47,6 +48,12 @@ export function createNodeFileSystem( if (readonly) { throw new Error("File system is readonly"); } + + const ext = getFullExtension(filePath); + const name = path.basename(filePath, ext); + const safeBase = makeSafeName(name, ext); + filePath = path.join(path.dirname(filePath), safeBase); + ensureInsideProjectRoot(filePath, normalizedRoot); if (fs.existsSync(filePath)) { ensureNotSymlink(filePath); diff --git a/src/vfs/createVirtualFileSystem.spec.ts b/src/vfs/createVirtualFileSystem.spec.ts index 7f2d4fde69..ba7997bd3b 100644 --- a/src/vfs/createVirtualFileSystem.spec.ts +++ b/src/vfs/createVirtualFileSystem.spec.ts @@ -27,4 +27,54 @@ describe("createVirtualFileSystem", () => { expect(vfs.exists(realPath)).toBe(true); expect(vfs.readFile(realPath).toString()).toBe(""); }); + + it("should truncate and hash long filenames", () => { + const fs: Record = {}; + const vfs = createVirtualFileSystem("@vroot", fs, false); + + const longName = "A".repeat(300); + const content = "Test content"; + const ext = ".md"; + + const inputPath = vfs.resolve("./", `${longName}${ext}`); + + vfs.writeFile(inputPath, content); + + const storedPaths = Object.keys(fs); + expect(storedPaths.length).toBe(1); + + const storedPath = storedPaths[0]!; + expect(storedPath).toBeDefined(); + expect(storedPath.length).toBeLessThanOrEqual(255); + + const regex = new RegExp( + `^${longName.slice(0, 255 - ext.length - 9)}_[0-9a-f]{8}${ext}$`, + ); + expect(storedPath).toMatch(regex); + + expect(fs[storedPath]).toBe(Buffer.from(content).toString("base64")); + }); + + it("should not truncate or hash short filenames", () => { + const fs: Record = {}; + const vfs = createVirtualFileSystem("@vroot", fs, false); + + const shortName = "short-filename"; + const content = "Test content"; + const ext = ".md"; + + const inputPath = vfs.resolve("./", `${shortName}${ext}`); + + vfs.writeFile(inputPath, content); + + const storedPaths = Object.keys(fs); + expect(storedPaths.length).toBe(1); + + const storedPath = storedPaths[0]!; + expect(storedPath).toBeDefined(); + + expect(storedPath).toBe(`${shortName}${ext}`); + expect(fs[storedPath]).toBe(Buffer.from(content).toString("base64")); + }); + }); diff --git a/src/vfs/createVirtualFileSystem.ts b/src/vfs/createVirtualFileSystem.ts index f07fe52496..1529a4278f 100644 --- a/src/vfs/createVirtualFileSystem.ts +++ b/src/vfs/createVirtualFileSystem.ts @@ -1,5 +1,7 @@ import normalize from "path-normalize"; import type { VirtualFileSystem } from "@/vfs/VirtualFileSystem"; +import path from "path"; +import { getFullExtension, makeSafeName } from "@/vfs/utils"; export function createVirtualFileSystem( root: string, @@ -47,7 +49,13 @@ export function createVirtualFileSystem( `Path '${filePath}' is outside of the root directory '${normalizedRoot}'`, ); } - const name = filePath.slice(normalizedRoot.length); + const relPath = filePath.slice(normalizedRoot.length); + const dir = path.dirname(relPath); + const ext = getFullExtension(relPath); + const base = path.basename(relPath, ext); + + const safeName = makeSafeName(base, ext); + const name = path.join(dir, safeName); fs[name] = typeof content === "string" ? Buffer.from(content).toString("base64") diff --git a/src/vfs/utils.ts b/src/vfs/utils.ts new file mode 100644 index 0000000000..0a01f8d1c3 --- /dev/null +++ b/src/vfs/utils.ts @@ -0,0 +1,41 @@ +import { sha256_sync } from "@ton/crypto"; +import path from "path"; + +/** + * Ensures the resulting file name does not exceed the given maximum length. + * If too long, trims the name and appends a short hash to avoid collisions. + * + * @param name - The base file name without extension. + * @param ext - The file extension. + * @param maxLen - Maximum allowed length for the full file name (default: 255). + * @returns A safe file name within the specified length. + */ +export const makeSafeName = ( + name: string, + ext: string, + maxLen = 255, +): string => { + const full = name + ext; + + if (full.length <= maxLen) { + return full; + } + + const hash = sha256_sync(Buffer.from(name)).toString("hex").slice(0, 8); + const suffix = `_${hash}${ext}`; + const maxNameLen = maxLen - suffix.length; + const safeName = name.slice(0, maxNameLen); + + return `${safeName}${suffix}`; +}; + +/** + * Returns the full extension of a file, including all parts after the first dot. + * - "file.txt" => ".txt" + * - "archive.tar.gz" => ".tar.gz" + */ +export const getFullExtension = (filename: string): string => { + const base = path.basename(filename); + const firstDotIndex = base.indexOf("."); + return firstDotIndex !== -1 ? base.slice(firstDotIndex) : ""; +}; From 1181dfe2cb2357dadc3af1a15a51969e938a5d9d Mon Sep 17 00:00:00 2001 From: hazyone Date: Fri, 23 May 2025 10:53:23 +0200 Subject: [PATCH 2/2] fix lint --- src/vfs/createVirtualFileSystem.spec.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/vfs/createVirtualFileSystem.spec.ts b/src/vfs/createVirtualFileSystem.spec.ts index ba7997bd3b..bde53ecf91 100644 --- a/src/vfs/createVirtualFileSystem.spec.ts +++ b/src/vfs/createVirtualFileSystem.spec.ts @@ -30,7 +30,7 @@ describe("createVirtualFileSystem", () => { it("should truncate and hash long filenames", () => { const fs: Record = {}; - const vfs = createVirtualFileSystem("@vroot", fs, false); + const vfs = createVirtualFileSystem("/", fs, false); const longName = "A".repeat(300); const content = "Test content"; @@ -57,7 +57,7 @@ describe("createVirtualFileSystem", () => { it("should not truncate or hash short filenames", () => { const fs: Record = {}; - const vfs = createVirtualFileSystem("@vroot", fs, false); + const vfs = createVirtualFileSystem("/", fs, false); const shortName = "short-filename"; const content = "Test content"; @@ -76,5 +76,4 @@ describe("createVirtualFileSystem", () => { expect(storedPath).toBe(`${shortName}${ext}`); expect(fs[storedPath]).toBe(Buffer.from(content).toString("base64")); }); - });