fs/ext: implement symlink support (#414)

Implement support for reading fast and slow symlinks to files and directories in the ext driver.

Add handling for nested symlinks and absolute/relative paths, with a max depth limit of 40.
Add `ERROR_TOO_MANY_LINKS = 40` to `fs_lfn.inc`.
Fix the driver to ignore multiple slashes in the path.

Reviewed-on: #414
Reviewed-by: Ivan B <1+dunkaist@noreply.localhost>
Reviewed-by: hidnplayr <hidnplayr@gmail.com>
Co-authored-by: Matou <mathieubotros@gmail.com>
Co-committed-by: Matou <mathieubotros@gmail.com>
This commit was merged in pull request #414.
This commit is contained in:
2026-05-15 19:21:30 +00:00
committed by Ivan B
parent 8235572f7b
commit e1a30a4f14
2 changed files with 170 additions and 2 deletions
+169 -2
View File
@@ -136,11 +136,15 @@ EXTENTS_USED = 80000h
TYPE_MASK = 0F000h
FLAG_FILE = 8000h
DIRECTORY = 4000h
FLAG_SYMLINK = 0A000h
DIR_FLAG_FILE = 1
DIR_DIRECTORY = 2
DIR_SYMLINK = 7
KOS_HIDDEN = 2
KOS_DIRECTORY = 10h
READ_ONLY = 1
SYMLINK_MAX_DEPTH = 40
FAST_SYMLINK_MAX_SIZE = 60
; Implemented "incompatible" features:
; 2 = have file type in directory entry
@@ -173,6 +177,8 @@ rootInodeBuffer INODE
align2 rb 600h-EXTFS.align2
inodeBuffer INODE
align3 rb 800h-EXTFS.align3
symlink_workspace rb maxPathLength
symlink_depth dd ?
ends
ext_report_unsupported_features:
@@ -1683,6 +1689,7 @@ findInode:
; [ebp+EXTFS.inodeBuffer] = last inode
; ecx = parent inode number
; CF=1 -> file not found, edi=0 -> error
mov dword [ebp+EXTFS.symlink_depth], SYMLINK_MAX_DEPTH
push esi
lea esi, [ebp+EXTFS.rootInodeBuffer]
lea edi, [ebp+EXTFS.inodeBuffer]
@@ -1694,6 +1701,12 @@ findInode:
mov edi, esi
cmp [edx+INODE.fileSize], 0
jz .not_found
.skip_root_slashes:
cmp byte [esi], '/'
jne @f
inc esi
jmp .skip_root_slashes
@@:
cmp byte [esi], 0
jnz .next_path_part
xor eax, eax
@@ -1744,7 +1757,10 @@ findInode:
je @f
cmp byte [esi], '/'
jne @b
.skip_slashes:
inc esi
cmp byte [esi], '/'
je .skip_slashes
@@:
pop edx
.stop:
@@ -1766,11 +1782,15 @@ findInode:
push eax
call readInode
jc .error
movzx eax, [ebx+INODE.accessMode]
and eax, TYPE_MASK
; check if symlink
cmp eax, FLAG_SYMLINK
jz .resolve_symlink
cmp byte [esi], 0
je .ret
mov edx, ebx
movzx eax, [ebx+INODE.accessMode]
and eax, TYPE_MASK
cmp eax, DIRECTORY
jz .next_path_part
xor edi, edi ; path folder is a file
@@ -1791,6 +1811,153 @@ findInode:
pop esi ecx ebx
ret
.resolve_symlink:
dec dword [ebp+EXTFS.symlink_depth]
jns @f
movi eax, ERROR_TOO_MANY_LINKS
jmp .error
@@:
cmp dword [ebp+EXTFS.symlink_depth], SYMLINK_MAX_DEPTH - 1
je .setup_extraction ; First hop - esi in shell memory
.nested_symlink:
; Symlink path is in symlink_workspace. esi points to the remaining path (tail).
; Shift tail by symlink size to prevent overwriting during insertion.
push edi ecx eax
; Find tail length
mov edi, esi
mov ecx, -1
xor al, al
repnz scasb
not ecx ; ecx = tail length including null
; Boundary overflow check
mov eax, [ebx+INODE.fileSize]
lea edi, [esi + eax]
add edi, ecx
lea eax, [ebp+EXTFS.symlink_workspace + maxPathLength]
cmp edi, eax
ja .error_nested
; Shift
mov eax, [ebx+INODE.fileSize]
lea edi, [esi + ecx - 1]
add edi, eax
lea esi, [esi + ecx - 1]
std
rep movsb
cld
lea esi, [edi + 1]
pop eax ecx edi
.setup_extraction:
push esi ; Save the remaining path
lea edi, [ebp+EXTFS.symlink_workspace]
cmp [ebx+INODE.fileSize], FAST_SYMLINK_MAX_SIZE
jbe .fast_symlink
; The target path is longer than 60 bytes, so it's stored in the file's data blocks.
.slow_symlink:
xor ecx, ecx ; Logical block 0 for extfsGetExtent
mov edx, [ebx+INODE.fileSize]
cmp edx, maxPathLength
jae .error_slow
push ecx edx edi
call extfsGetExtent
jc .error_slow
mov ebx, [ebp+EXTFS.tempBlockBuffer]
call extfsReadBlock
jc .error_slow
pop edi edx ecx
push esi
mov esi, [ebp+EXTFS.tempBlockBuffer]
mov ecx, edx
rep movsb
pop esi
jmp .symlink_merge
; The target path is short enough, it's stored in the inode's block numbers.
.fast_symlink:
lea esi, [ebx+INODE.blockNumbers]
mov ecx, [ebx+INODE.fileSize]
rep movsb
.symlink_merge:
pop esi ; Restore the remaining path
cmp byte [esi], 0
jne .symlink_copy_remaining
mov byte [edi], 0
jmp .symlink_check_type
; Copy the remaining path to the symlink workspace
.symlink_copy_remaining:
; Skip adding '/' if the symlink already ends with it
cmp byte [edi-1], '/'
je @f
mov byte [edi], '/'
inc edi
@@:
lodsb
stosb
test al, al
jnz @b
.symlink_check_type:
lea esi, [ebp+EXTFS.symlink_workspace]
cmp byte [esi], '/'
jne .relative_symlink
; The target path starts with '/', it is absolute.
inc esi
mov dword [esp], ROOT_INODE
mov dword [esp+4], ROOT_INODE
push esi
lea esi, [ebp+EXTFS.rootInodeBuffer]
lea edi, [ebp+EXTFS.inodeBuffer]
movzx ecx, [ebp+EXTFS.superblock.inodeSize]
rep movsb
pop esi
lea edx, [ebp+EXTFS.inodeBuffer]
cmp byte [esi], 0
jz .ret
jmp .next_path_part
; The target path does not start with '/', it is relative to the current directory.
.relative_symlink:
mov eax, [esp+4]
mov [esp], eax
lea ebx, [ebp+EXTFS.inodeBuffer]
push eax
call readInode
add esp, 4
jc .error
mov edx, ebx
jmp .next_path_part
.error_nested:
pop eax ecx edi
jmp .error
.error_slow:
add esp, 12
pop esi
jmp .error
writeSuperblock:
push ebx
mov eax, 2
+1
View File
@@ -18,6 +18,7 @@ ERROR_DISK_FULL = 8
ERROR_FS_FAIL = 9
ERROR_ACCESS_DENIED = 10
ERROR_DEVICE = 11
ERROR_TOO_MANY_LINKS = 40
maxPathLength = 1000h