Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for symlinks to fake filesystem #48

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 79 additions & 10 deletions src/try/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,27 @@
const enum Kind {
File,
Directory,
Symlink,
}

const enum StatsMode {
IFLNK = 0o120000,
IFREG = 0o100000,
IFDIR = 0o40000,
}

type Entry = File | Directory
function kindToStats(kind: Kind): StatsMode {
switch (kind) {
case Kind.File:
return StatsMode.IFREG;
case Kind.Directory:
return StatsMode.IFDIR;
case Kind.Symlink:
return StatsMode.IFLNK;
}
}

type Entry = File | Directory | Symlink

interface Metadata {
inode_: number
Expand All @@ -30,6 +43,11 @@ interface Directory extends Metadata {
children_: Map<string, Entry>
}

interface Symlink extends Metadata {
kind_: Kind.Symlink
target: string
}

class Stats {
declare dev: number
declare ino: number
Expand Down Expand Up @@ -57,7 +75,7 @@ class Stats {
const ctimeMs = entry.ctime_.getTime()
this.dev = 1
this.ino = entry.inode_
this.mode = entry.kind_ === Kind.File ? StatsMode.IFREG : StatsMode.IFDIR
this.mode = kindToStats(entry.kind_);
this.nlink = 1
this.uid = 1
this.gid = 1
Expand All @@ -82,6 +100,10 @@ class Stats {
isFile(): boolean {
return this.mode === StatsMode.IFREG
}

isSymbolicLink(): boolean {
return this.mode === StatsMode.IFLNK
}
}

interface Handle {
Expand All @@ -97,9 +119,9 @@ const ENOTDIR = errorWithCode('ENOTDIR')
const handles = new Map<number, Handle>()
const encoder = new TextEncoder
const decoder = new TextDecoder
let root: Directory = createDirectory()
let nextFD = 3
let nextInode = 1
let root: Directory = createDirectory()
export let stderrSinceReset = ''

// The "esbuild-wasm" package overwrites "fs.writeSync" with this value
Expand Down Expand Up @@ -136,6 +158,8 @@ function read(
callback(EBADF, 0, buffer)
} else if (handle.entry_.kind_ === Kind.Directory) {
callback(EISDIR, 0, buffer)
} else if (handle.entry_.kind_ === Kind.Symlink) {
callback(EINVAL, 0, buffer);
} else {
const content = handle.entry_.content_
if (position !== null && position !== -1) {
Expand All @@ -160,7 +184,15 @@ export function resetFileSystem(files: Record<string, string>): void {
root.children_.clear()
stderrSinceReset = ''

const records: Array<{ path: string, entry: Entry }> = [];
for (const path in files) {
records.push({ path, entry: createFile(encoder.encode(files[path])) });
}
populateFileSystem(records);
}

function populateFileSystem(records: Array<{ path: string, entry: Entry }>): void {
for (const { path, entry } of records) {
const parts = splitPath(absoluteNormalizedPath(path))
let dir = root

Expand All @@ -178,8 +210,16 @@ export function resetFileSystem(files: Record<string, string>): void {

const part = parts[parts.length - 1]
if (dir.children_.has(part)) rejectConflict(part)
dir.children_.set(part, createFile(encoder.encode(files[path])))
dir.children_.set(part, entry)
}
}

export function createSymlinks(links: Record<string, string>): void {
const records: Array<{ path: string, entry: Entry }> = [];
for (const path in links) {
records.push({ path, entry: createSymlink(links[path]) });
}
populateFileSystem(records);
}

globalThis.fs = {
Expand All @@ -203,7 +243,7 @@ globalThis.fs = {
callback: (err: Error | null, fd: number | null) => void,
) {
try {
const entry = getEntryFromPath(path)
const entry = getEntryFromPathFollowingSymlinks(path, false)
const fd = nextFD++
handles.set(fd, { entry_: entry, offset_: 0 })
callback(null, fd)
Expand Down Expand Up @@ -231,7 +271,7 @@ globalThis.fs = {

readdir(path: string, callback: (err: Error | null, files: string[] | null) => void) {
try {
const entry = getEntryFromPath(path)
const entry = getEntryFromPathFollowingSymlinks(path, false)
if (entry.kind_ !== Kind.Directory) throw ENOTDIR
callback(null, [...entry.children_.keys()])
} catch (err) {
Expand All @@ -241,7 +281,7 @@ globalThis.fs = {

stat(path: string, callback: (err: Error | null, stats: Stats | null) => void) {
try {
const entry = getEntryFromPath(path)
const entry = getEntryFromPathFollowingSymlinks(path, false)
callback(null, new Stats(entry))
} catch (err) {
callback(err, null)
Expand All @@ -250,7 +290,7 @@ globalThis.fs = {

lstat(path: string, callback: (err: Error | null, stats: Stats | null) => void) {
try {
const entry = getEntryFromPath(path)
const entry = getEntryFromPathFollowingSymlinks(path, true)
callback(null, new Stats(entry))
} catch (err) {
callback(err, null)
Expand All @@ -265,6 +305,19 @@ globalThis.fs = {
callback(EBADF, null)
}
},

readlink(path: string, callback: (err: Error | null, linkString: string) => void) {
try {
const entry = getEntryFromPathFollowingSymlinks(path, true)
if (entry.kind_ === Kind.Symlink) {
callback(null, entry.target);
} else {
callback(EINVAL, null)
}
} catch (err) {
callback(err, null)
}
},
}

function createFile(content: Uint8Array): File {
Expand All @@ -289,6 +342,17 @@ function createDirectory(): Directory {
}
}

function createSymlink(target: string): Symlink {
const now = new Date
return {
kind_: Kind.Symlink,
inode_: nextInode++,
ctime_: now,
mtime_: now,
target,
}
}

function absoluteNormalizedPath(path: string): string {
if (path[0] !== '/') path = '/' + path
const parts = path.split('/')
Expand All @@ -314,7 +378,7 @@ function splitPath(path: string): string[] {
return parts
}

function getEntryFromPath(path: string): Entry {
function getEntryFromPathFollowingSymlinks(path: string, returnLink: boolean): Entry {
const parts = splitPath(path)
let dir = root
for (let i = 0, n = parts.length; i < n; i++) {
Expand All @@ -324,7 +388,12 @@ function getEntryFromPath(path: string): Entry {
if (i + 1 === n) return child
throw ENOTDIR
}
dir = child
if (child.kind_ === Kind.Symlink) {
if (returnLink && i + 1 === n) return child
dir = getEntryFromPathFollowingSymlinks(child.target, false)
} else {
dir = child
}
}
return dir
}
Expand Down