Showing the changes to apps/server/src/workspace/WorkspaceFileSystem.ts (1 of 2 files). Show all
Unified · Split
| @@ -24,6 +24,46 @@ import * as WorkspacePaths from "./WorkspacePaths.ts"; · 23 unmodified lines | ||
| 24 | 24 | |
| 25 | 25 | const PROJECT_READ_FILE_MAX_BYTES = 1024 * 1024; |
| 26 | 26 | |
| 27 | +class WorkspaceReadFileResolvedOutsideRootError extends Schema.TaggedErrorClass<WorkspaceReadFileResolvedOutsideRootError>()( | |
| 28 | + "WorkspaceReadFileResolvedOutsideRootError", | |
| 29 | + { | |
| 30 | + cwd: Schema.String, | |
| 31 | + relativePath: Schema.String, | |
| 32 | + realWorkspaceRoot: Schema.String, | |
| 33 | + realTargetPath: Schema.String, | |
| 34 | + }, | |
| 35 | +) { | |
| 36 | + override get message(): string { | |
| 37 | + return "Workspace file path resolves outside the project root."; | |
| 38 | + } | |
| 39 | +} | |
| 40 | + | |
| 41 | +class WorkspaceReadFileNotFileError extends Schema.TaggedErrorClass<WorkspaceReadFileNotFileError>()( | |
| 42 | + "WorkspaceReadFileNotFileError", | |
| 43 | + { | |
| 44 | + cwd: Schema.String, | |
| 45 | + relativePath: Schema.String, | |
| 46 | + fileType: Schema.String, | |
| 47 | + }, | |
| 48 | +) { | |
| 49 | + override get message(): string { | |
| 50 | + return "Workspace path is not a file."; | |
| 51 | + } | |
| 52 | +} | |
| 53 | + | |
| 54 | +class WorkspaceReadFileBinaryFileError extends Schema.TaggedErrorClass<WorkspaceReadFileBinaryFileError>()( | |
| 55 | + "WorkspaceReadFileBinaryFileError", | |
| 56 | + { | |
| 57 | + cwd: Schema.String, | |
| 58 | + relativePath: Schema.String, | |
| 59 | + nulByteOffset: Schema.Number, | |
| 60 | + }, | |
| 61 | +) { | |
| 62 | + override get message(): string { | |
| 63 | + return "Binary files cannot be previewed as text."; | |
| 64 | + } | |
| 65 | +} | |
| 66 | + | |
| 27 | 67 | export class WorkspaceFileSystemError extends Schema.TaggedErrorClass<WorkspaceFileSystemError>()( |
| 28 | 68 | "WorkspaceFileSystemError", |
| 29 | 69 | { |
| @@ -96,22 +136,40 @@ export const make = Effect.gen(function* () { · 66 unmodified lines | ||
| 96 | 136 | path.isAbsolute(relativeRealPath) |
| 97 | 137 | ) { |
| 98 | 138 | return yield* Effect.fail( |
| 99 | − new Error("Workspace file path resolves outside the project root."), | |
| 139 | + new WorkspaceReadFileResolvedOutsideRootError({ | |
| 140 | + cwd: input.cwd, | |
| 141 | + relativePath: target.relativePath, | |
| 142 | + realWorkspaceRoot, | |
| 143 | + realTargetPath, | |
| 144 | + }), | |
| 100 | 145 | ); |
| 101 | 146 | } |
| 102 | 147 | |
| 103 | 148 | const handle = yield* fileSystem.open(realTargetPath, { flag: "r" }); |
| 104 | 149 | const stat = yield* handle.stat; |
| 105 | 150 | if (stat.type !== "File") { |
| 106 | − return yield* Effect.fail(new Error("Workspace path is not a file.")); | |
| 151 | + return yield* Effect.fail( | |
| 152 | + new WorkspaceReadFileNotFileError({ | |
| 153 | + cwd: input.cwd, | |
| 154 | + relativePath: target.relativePath, | |
| 155 | + fileType: stat.type, | |
| 156 | + }), | |
| 157 | + ); | |
| 107 | 158 | } |
| 108 | 159 | const byteLength = Number(stat.size); |
| 109 | 160 | const bytesToRead = Math.min(byteLength, PROJECT_READ_FILE_MAX_BYTES); |
| 110 | 161 | const buffer = new Uint8Array(bytesToRead); |
| 111 | 162 | const bytesRead = yield* handle.read(buffer); |
| 112 | 163 | const fileBytes = buffer.subarray(0, Number(bytesRead)); |
| 113 | − if (fileBytes.includes(0)) { | |
| 114 | − return yield* Effect.fail(new Error("Binary files cannot be previewed as text.")); | |
| 164 | + const nulByteOffset = fileBytes.indexOf(0); | |
| 165 | + if (nulByteOffset !== -1) { | |
| 166 | + return yield* Effect.fail( | |
| 167 | + new WorkspaceReadFileBinaryFileError({ | |
| 168 | + cwd: input.cwd, | |
| 169 | + relativePath: target.relativePath, | |
| 170 | + nulByteOffset, | |
| 171 | + }), | |
| 172 | + ); | |
| 115 | 173 | } |
| 116 | 174 | const contents = new TextDecoder("utf-8").decode(fileBytes); |
| 117 | 175 | return { |