feat: add local access control and managed point clouds

This commit is contained in:
2026-07-22 11:05:48 +08:00
parent 683e4d6c07
commit 8555aa2f2e
46 changed files with 1982 additions and 884 deletions

View File

@@ -0,0 +1,125 @@
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
inspectPointcloudMetadata,
MAX_PLY_HEADER_BYTES,
parsePlyHeader,
PointcloudMetadataParseError,
} from "./pointcloudMetadata";
const temporaryDirectories: string[] = [];
afterEach(() => {
while (temporaryDirectories.length > 0) {
rmSync(temporaryDirectories.pop()!, { recursive: true, force: true });
}
});
function header(lines: string[], newline = "\n") {
return Buffer.from([...lines, "end_header", ""].join(newline), "ascii");
}
describe("parsePlyHeader", () => {
it("解析 ASCII PLY,并以 element vertex 作为精确点数", () => {
const result = parsePlyHeader(header([
"ply",
"format ascii 1.0",
"element vertex 1000000000",
"property float x",
"property float y",
"property double z",
"element face 999",
"property list uchar int vertex_indices",
]));
expect(result).toEqual({
pointCount: 1_000_000_000,
pointDataFormat: "ascii",
bytesPerPoint: 16,
pointAttributes: [
{ name: "x", type: "float32", bytes: 4 },
{ name: "y", type: "float32", bytes: 4 },
{ name: "z", type: "float64", bytes: 8 },
],
});
});
it.each([
["binary_little_endian", "binary_little_endian"],
["binary_big_endian", "binary_big_endian"],
] as const)("解析 %s PLY", (format, expected) => {
const result = parsePlyHeader(header([
"ply",
`format ${format} 1.0`,
"element vertex 42",
"property int16 x",
"property ushort intensity",
"property uchar red",
]));
expect(result.pointDataFormat).toBe(expected);
expect(result.bytesPerPoint).toBe(5);
});
it("支持 CRLF 文件头和类型别名", () => {
const result = parsePlyHeader(header([
"ply",
"format ascii 1.0",
"comment generated",
"element vertex 3",
"property char classification",
"property uint confidence",
"property float32 temperature",
], "\r\n"));
expect(result.pointAttributes).toEqual([
{ name: "classification", type: "int8", bytes: 1 },
{ name: "confidence", type: "uint32", bytes: 4 },
{ name: "temperature", type: "float32", bytes: 4 },
]);
});
it("拒绝无效文件头", () => {
expect(() => parsePlyHeader(Buffer.from("not-ply\n"))).toThrow(PointcloudMetadataParseError);
});
it("拒绝未知的顶点属性类型", () => {
expect(() => parsePlyHeader(header([
"ply",
"format ascii 1.0",
"element vertex 1",
"property half x",
]))).toThrow("未知的顶点属性类型");
});
it("拒绝列表型顶点属性,但忽略其他 element 的列表属性", () => {
expect(() => parsePlyHeader(header([
"ply",
"format ascii 1.0",
"element vertex 1",
"property list uchar float samples",
]))).toThrow("列表型顶点属性");
});
it("非 PLY 格式直接标记为 unsupported,不读取文件", () => {
expect(inspectPointcloudMetadata("/path/does/not/exist.txt", "cloud.txt")).toEqual({
metadataStatus: "unsupported",
pointCount: null,
pointAttributes: null,
pointDataFormat: null,
bytesPerPoint: null,
metadataError: null,
});
});
it("只读取 16KB 文件头,超长文件头标记为解析失败", () => {
const directory = mkdtempSync(join(tmpdir(), "pointcloud-metadata-"));
temporaryDirectories.push(directory);
const filePath = join(directory, "oversized.ply");
writeFileSync(filePath, `ply\nformat ascii 1.0\ncomment ${"x".repeat(MAX_PLY_HEADER_BYTES)}\nend_header\n`);
const result = inspectPointcloudMetadata(filePath, "oversized.ply");
expect(result.metadataStatus).toBe("error");
expect(result.metadataError).toContain("16KB");
});
});