GitRoot

Craft your forge, Build your project, Grow your community freely
  1// SPDX-FileCopyrightText: 2025 Romain Maneschi <romain@gitroot.dev>
  2//
  3// SPDX-License-Identifier: MIT
  4
  5import {
  6  dircookie,
  7  errno,
  8  fd,
  9  fd_readdir,
 10  fdflags,
 11  filestat,
 12  lookupflags,
 13  oflags,
 14  path_filestat_get,
 15  path_open,
 16  rights,
 17} from "@assemblyscript/wasi-shim/assembly/bindings/wasi_snapshot_preview1";
 18import { Descriptor } from "as-wasi/assembly";
 19import {
 20  _copyFile,
 21  _deleteFile,
 22  _moveFile,
 23  _replaceContent,
 24  _writeContent,
 25} from "./imports";
 26import { errorOrNull } from "./wasm";
 27
 28export const FsBaseWorktree = "worktree";
 29export const FsBaseWebcontent = "webcontent";
 30export const FsBaseCache = "cache";
 31
 32export class FS {
 33  private fs: FileSystem = new FileSystem(3);
 34  private base: string;
 35
 36  constructor(base: string) {
 37    this.base = base;
 38  }
 39
 40  readDir(path: string): string[] | null {
 41    const dirs = this.fs.readdir(this.base + "/" + path);
 42    if (dirs == null) {
 43      return [];
 44    }
 45    return dirs;
 46  }
 47
 48  readAll(path: string): string | null {
 49    const fd = this.fs.open(this.base + "/" + path);
 50    if (fd == null) {
 51      return null;
 52    }
 53    const data = fd.readString();
 54    if (data == null) {
 55      return null;
 56    }
 57    fd.close();
 58    return data;
 59  }
 60
 61  readLine(path: string): File | null {
 62    const fd = this.fs.open(this.base + "/" + path);
 63    if (fd == null) {
 64      return null;
 65    }
 66    return new File(fd);
 67  }
 68
 69  exists(path: string): bool {
 70    return this.fs.exists(this.base + "/" + path);
 71  }
 72
 73  writeContent(path: string, content: string): Error | null {
 74    return errorOrNull(_writeContent(this.base, path, content));
 75  }
 76
 77  replaceContent(
 78    path: string,
 79    oldContent: string,
 80    content: string,
 81  ): Error | null {
 82    return errorOrNull(_replaceContent(this.base, path, oldContent, content));
 83  }
 84
 85  copyFile(fromPath: string, toPath: string): Error | null {
 86    return errorOrNull(_copyFile(this.base, fromPath, this.base, toPath));
 87  }
 88
 89  copyFileToFs(fromPath: string, toFs: FS, toPath: string): Error | null {
 90    return errorOrNull(_copyFile(this.base, fromPath, toFs.base, toPath));
 91  }
 92
 93  deleteFile(filename: string): Error | null {
 94    return errorOrNull(_deleteFile(this.base, filename));
 95  }
 96
 97  moveFile(fromPath: string, toPath: string): Error | null {
 98    return errorOrNull(_moveFile(this.base, fromPath, this.base, toPath));
 99  }
100
101  moveFileToFs(fromPath: string, toFs: FS, toPath: string): Error | null {
102    return errorOrNull(_moveFile(this.base, fromPath, toFs.base, toPath));
103  }
104}
105
106class File {
107  constructor(private fd: Descriptor) {}
108
109  readLine(): string | null {
110    return this.fd.readLine();
111  }
112
113  close(): void {
114    this.fd.close();
115  }
116}
117
118/**
119 * ######################################
120 * COPIED FROM AS-WASI
121 * ######################################
122 */
123
124/**
125 * A class to access a filesystem
126 */
127class FileSystem {
128  constructor(private dirfd: u32) {}
129
130  /**
131   * Open a path
132   * @path path
133   * @flags r, r+, w, wx, w+ or xw+
134   * @returns a descriptor
135   */
136  open(path: string, flags: string = "r"): Descriptor | null {
137    let dirfd = this.dirfdForPath(path);
138    let fd_lookup_flags = lookupflags.SYMLINK_FOLLOW;
139    let fd_oflags: u16 = 0;
140    let fd_rights: u64 = 0;
141    if (flags == "r") {
142      fd_rights =
143        rights.FD_READ |
144        rights.FD_SEEK |
145        rights.FD_TELL |
146        rights.FD_FILESTAT_GET |
147        rights.FD_READDIR;
148    } else if (flags == "r+") {
149      fd_rights =
150        rights.FD_WRITE |
151        rights.FD_READ |
152        rights.FD_SEEK |
153        rights.FD_TELL |
154        rights.FD_FILESTAT_GET |
155        rights.PATH_CREATE_FILE;
156    } else if (flags == "w") {
157      fd_oflags = oflags.CREAT | oflags.TRUNC;
158      fd_rights =
159        rights.FD_WRITE |
160        rights.FD_SEEK |
161        rights.FD_TELL |
162        rights.FD_FILESTAT_GET |
163        rights.PATH_CREATE_FILE;
164    } else if (flags == "wx") {
165      fd_oflags = oflags.CREAT | oflags.TRUNC | oflags.EXCL;
166      fd_rights =
167        rights.FD_WRITE |
168        rights.FD_SEEK |
169        rights.FD_TELL |
170        rights.FD_FILESTAT_GET |
171        rights.PATH_CREATE_FILE;
172    } else if (flags == "w+") {
173      fd_oflags = oflags.CREAT | oflags.TRUNC;
174      fd_rights =
175        rights.FD_WRITE |
176        rights.FD_READ |
177        rights.FD_SEEK |
178        rights.FD_TELL |
179        rights.FD_FILESTAT_GET |
180        rights.PATH_CREATE_FILE;
181    } else if (flags == "xw+") {
182      fd_oflags = oflags.CREAT | oflags.TRUNC | oflags.EXCL;
183      fd_rights =
184        rights.FD_WRITE |
185        rights.FD_READ |
186        rights.FD_SEEK |
187        rights.FD_TELL |
188        rights.FD_FILESTAT_GET |
189        rights.PATH_CREATE_FILE;
190    } else {
191      return null;
192    }
193    let fd_rights_inherited = fd_rights;
194    let fd_flags: fdflags = 0;
195    let path_utf8_buf = String.UTF8.encode(path);
196    let path_utf8_len: usize = path_utf8_buf.byteLength;
197    let path_utf8 = changetype<usize>(path_utf8_buf);
198    let fd_buf = memory.data(8);
199    let res = path_open(
200      dirfd as fd,
201      fd_lookup_flags,
202      path_utf8,
203      path_utf8_len,
204      fd_oflags,
205      fd_rights,
206      fd_rights_inherited,
207      fd_flags,
208      fd_buf,
209    );
210    if (res !== errno.SUCCESS) {
211      return null;
212    }
213    let fd = load<u32>(fd_buf);
214    return new Descriptor(fd);
215  }
216
217  /**
218   * Check if a file exists at a given path
219   * @path path
220   * @returns `true` on success, `false` on failure
221   */
222  exists(path: string): bool {
223    let dirfd = this.dirfdForPath(path);
224    let path_utf8_buf = String.UTF8.encode(path);
225    let path_utf8_len: usize = path_utf8_buf.byteLength;
226    let path_utf8 = changetype<usize>(path_utf8_buf);
227    let fd_lookup_flags = lookupflags.SYMLINK_FOLLOW;
228    let st_buf = changetype<usize>(new ArrayBuffer(64));
229    let res = path_filestat_get(
230      dirfd,
231      fd_lookup_flags,
232      path_utf8,
233      path_utf8_len,
234      changetype<filestat>(st_buf),
235    );
236    return res === errno.SUCCESS;
237  }
238
239  /**
240   * Get the content of a directory
241   * @param path the directory path
242   * @returns An array of file names
243   */
244  readdir(path: string): Array<string> | null {
245    let fd = this.open(path, "r");
246    if (fd === null) {
247      return null;
248    }
249    let out = new Array<string>();
250    let buf_size = 4096;
251    // @ts-ignore
252    let buf = heap.alloc(buf_size);
253    // @ts-ignore
254    let buf_used_p = memory.data(8);
255    let buf_used = 0;
256    for (;;) {
257      if (
258        fd_readdir(fd.rawfd, buf, buf_size, 0 as dircookie, buf_used_p) !==
259        errno.SUCCESS
260      ) {
261        fd.close();
262      }
263      buf_used = load<u32>(buf_used_p);
264      if (buf_used < buf_size) {
265        break;
266      }
267      buf_size <<= 1;
268      // @ts-ignore
269      buf = heap.realloc(buf, buf_size);
270    }
271    let offset = 0;
272    while (offset < buf_used) {
273      offset += 16;
274      let name_len = load<u32>(buf + offset);
275      offset += 8;
276      if (offset + name_len > buf_used) {
277        return null;
278      }
279      let name = String.UTF8.decodeUnsafe(buf + offset, name_len);
280      out.push(name);
281      offset += name_len;
282    }
283    // @ts-ignore
284    heap.free(buf);
285    fd.close();
286
287    return out;
288  }
289
290  dirfdForPath(path: string): fd {
291    return this.dirfd;
292  }
293}