Skip to content

Commit b07e0a9

Browse files
authored
Generate htree image for testing (#21)
* Generate htree image for testing * Add more htree testing and better typing * Get tests working * Add inode_at to directories as well * Add inode_at to directories as well * Fix tests * Fix lint * Review feedback * Review feedback
1 parent 803c69d commit b07e0a9

9 files changed

Lines changed: 260 additions & 58 deletions

File tree

.github/workflows/build.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ jobs:
4747
test32.ext4.tmp
4848
test64.ext4
4949
test64.ext4.tmp
50+
test_htree.ext4
5051
if-no-files-found: error
5152
test:
5253
name: Test on ${{ matrix.os }} python ${{ matrix.python }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,4 @@ cython_debug/
160160
#.idea/
161161

162162
*.ext4
163+
*.ext4.tmp

_test_image.sh

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,60 @@
11
#!/bin/bash
22
set -e
33

4+
if ! command -v chronic &>/dev/null; then
5+
chronic() {
6+
"$@"
7+
}
8+
fi
9+
10+
mkimage() {
11+
name="$1"
12+
shift
13+
dir="$1"
14+
shift
15+
size="$1"
16+
shift
17+
18+
echo "[test] Making image $name < $dir..."
19+
chronic dd if=/dev/zero of="$name".ext4.tmp count="$size" bs=1048576
20+
chronic mkfs.ext4 -g 1024 "$@" "$name".ext4.tmp -d "$dir"
21+
echo -n F >"$name".ext4
22+
cat "$name".ext4.tmp >>"$name".ext4
23+
}
24+
425
tmp_dir=$(mktemp -d)
26+
# shellcheck disable=SC2064
527
trap "rm -r \"$tmp_dir\"" EXIT
6-
echo "hello world" > "$tmp_dir"/test.txt
7-
for i in {1..1000};do
8-
echo "hello world" >> "$tmp_dir"/test.txt
9-
done
10-
for i in {1..100};do
11-
echo "hello world$i" > "$tmp_dir"/test$i.txt
12-
for j in {1..20};do
13-
setfattr -n user.name$j -v value${i}_$j "$tmp_dir"/test$i.txt
28+
echo "[test] Using temporary directory: $tmp_dir"
29+
echo "[test] Generating files..."
30+
echo "hello world" >"$tmp_dir"/test.txt
31+
for i in {1..1000}; do
32+
echo "echo "hello world" >>'$tmp_dir/test.txt'"
33+
done | xargs -P "$(nproc)" -I {} bash -c '{}'
34+
for i in {1..100}; do
35+
echo "echo 'hello world$i' >'$tmp_dir/test$i.txt'"
36+
done | xargs -P "$(nproc)" -I {} bash -c '{}'
37+
for i in {1..100}; do
38+
for j in {1..20}; do
39+
echo "setfattr -n 'user.name$j' -v 'value${i}_$j' '$tmp_dir/test$i.txt'"
1440
done
15-
done
16-
dd if=/dev/zero of=test32.ext4.tmp count=20 bs=1048576
17-
dd if=/dev/zero of=test64.ext4.tmp count=20 bs=1048576
18-
mkfs.ext4 -g 1024 -O 64bit test64.ext4.tmp -d "$tmp_dir"
19-
mkfs.ext4 -g 1024 -O ^64bit test32.ext4.tmp -d "$tmp_dir"
20-
echo -n F > test32.ext4
21-
cat test32.ext4.tmp >> test32.ext4
22-
echo -n F > test64.ext4
23-
cat test64.ext4.tmp >> test64.ext4
41+
done | xargs -P "$(nproc)" -I {} bash -c '{}'
42+
43+
mkimage test32 "$tmp_dir" 20 -O ^64bit
44+
mkimage test64 "$tmp_dir" 20 -O 64bit
45+
46+
rm -f "$tmp_dir"/test*.txt
47+
echo "[test] Generating files..."
48+
49+
echo "[test] Making image test_htree..."
50+
chronic dd if=/dev/zero of=test_htree.ext4 count=20 bs=1048576
51+
chronic mkfs.ext4 -g 1024 -b 1024 -O 64bit,dir_index test_htree.ext4
52+
sudo mount -t ext4 test_htree.ext4 "$tmp_dir"
53+
# shellcheck disable=SC2064
54+
trap "sudo umount \"$tmp_dir\";rmdir \"$tmp_dir\"" EXIT
55+
sudo mkdir "$tmp_dir"/empty
56+
printf '%s\n' "$tmp_dir"/{1..200} | xargs sudo touch
57+
sudo umount "$tmp_dir"
58+
# shellcheck disable=SC2064
59+
trap "rmdir \"$tmp_dir\"" EXIT
60+
chronic e2fsck -Dy test_htree.ext4

ext4/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@
6969
from .xattr import ExtendedAttributeEntry
7070

7171
from .htree import DXRoot
72+
from .htree import DotDirectoryEntry2
73+
from .htree import DXEntry
74+
from .htree import DXRootInfo
7275

7376
__all__ = [
7477
"DX_HASH",
@@ -132,4 +135,7 @@
132135
"ExtendedAttributeHeader",
133136
"ExtendedAttributeEntry",
134137
"DXRoot",
138+
"DotDirectoryEntry2",
139+
"DXEntry",
140+
"DXRootInfo",
135141
]

ext4/htree.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from ctypes import LittleEndianStructure
1212

1313
from typing import final
14-
from typing import cast
1514
from typing import TYPE_CHECKING
1615

1716
from collections.abc import Generator
@@ -28,7 +27,18 @@
2827

2928

3029
class LittleEndianStructureWithVolume(LittleEndianStructure):
31-
volume: "Volume | None" = None
30+
def __init__(self):
31+
super().__init__()
32+
self._volume: "Volume | None" = None
33+
34+
@property
35+
def volume(self) -> "Volume":
36+
assert self._volume is not None
37+
return self._volume
38+
39+
@volume.setter
40+
def volume(self, volume: "Volume") -> None:
41+
self._volume = volume
3242

3343

3444
@final
@@ -45,7 +55,7 @@ class DotDirectoryEntry2(LittleEndianStructureWithVolume):
4555

4656
def verify(self) -> None:
4757
name = assert_cast(self.name, bytes) # pyright: ignore[reportAny]
48-
if name in (b".\0\0\0", b"..\0\0"):
58+
if name in (b".", b".."):
4959
return
5060

5161
message = f"{self} dot or dotdot entry name invalid! actual={name}"
@@ -141,8 +151,6 @@ class DXRoot(DXEntriesBase):
141151

142152
def __init__(self, inode: "Directory"):
143153
super().__init__(inode, 0)
144-
cast(DotDirectoryEntry2, self.dot).volume = inode.volume
145-
cast(DotDirectoryEntry2, self.dotdot).volume = inode.volume
146154

147155

148156
@final

ext4/inode.py

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from __future__ import annotations
33

44
import io
5+
import os
6+
import errno
57
import warnings
68

79
from ctypes import LittleEndianStructure
@@ -12,8 +14,12 @@
1214

1315
from typing import cast
1416
from typing import final
17+
from typing import Any
1518
from typing import TYPE_CHECKING
1619

20+
from cachetools import cachedmethod
21+
from cachetools import LRUCache
22+
1723
from ._compat import override
1824
from ._compat import ReadableStream
1925
from ._compat import assert_cast
@@ -437,9 +443,11 @@ def readlink(self):
437443
class Directory(Inode):
438444
def __init__(self, volume: "Volume", offset: int, i_no: int):
439445
super().__init__(volume, offset, i_no)
446+
self._inode_at_cache: LRUCache[str | bytes, Inode] = LRUCache(maxsize=32)
440447
self._dirents: None | list[DirectoryEntry | DirectoryEntry2] = None
448+
self.htree: DXRoot | None = None
441449
if self.is_htree:
442-
self.htree: DXRoot | None = DXRoot(self)
450+
self.htree = DXRoot(self)
443451

444452
@override
445453
def verify(self):
@@ -471,7 +479,9 @@ def is_encrypted(self) -> bool:
471479
def hash_in_dirent(self) -> bool:
472480
return self.is_casefolded and self.is_encrypted
473481

474-
def _opendir(self):
482+
def _opendir(
483+
self,
484+
) -> Generator[DirectoryEntry | DirectoryEntry2, None, None]:
475485
if self._dirents is not None:
476486
for dirent in self._dirents:
477487
yield dirent
@@ -554,7 +564,9 @@ def _get_file_type(self, dirent: DirectoryEntry | DirectoryEntry2) -> EXT4_FT:
554564
f"Unexpected file type {file_type} for inode {dirent_inode}"
555565
)
556566

557-
def opendir(self):
567+
def opendir(
568+
self,
569+
) -> Generator[tuple[DirectoryEntry | DirectoryEntry2, EXT4_FT], Any, None]: # pyright: ignore[reportExplicitAny]
558570
for dirent in self._opendir():
559571
if isinstance(dirent, DirectoryEntry2):
560572
file_type = assert_cast(dirent.file_type, EXT4_FT) # pyright: ignore[reportAny]
@@ -568,3 +580,37 @@ def opendir(self):
568580
file_type = self._get_file_type(dirent)
569581

570582
yield dirent, file_type
583+
584+
@cachedmethod(lambda self: self._inode_at_cache) # pyright: ignore[reportAny]
585+
def inode_at(self, path: str | bytes) -> Inode:
586+
if (isinstance(path, str) and path.startswith("/")) or (
587+
isinstance(path, bytes) and path.startswith(b"/")
588+
):
589+
return self.volume.inode_at(path)
590+
591+
if isinstance(path, bytes):
592+
path = path.decode("utf-8")
593+
594+
paths = list(self.volume.path_tuple(f"/{path}"))
595+
cwd = self
596+
if not paths:
597+
return cwd
598+
599+
while paths:
600+
if not isinstance(cwd, Directory):
601+
raise OSError(errno.ENOTDIR, os.strerror(errno.ENOTDIR))
602+
603+
name = paths.pop(0)
604+
inode = None
605+
for dirent, _ in cwd.opendir():
606+
if dirent.name_bytes == name:
607+
dirent_inode = assert_cast(dirent.inode, int) # pyright: ignore[reportAny]
608+
inode = self.volume.inodes[dirent_inode]
609+
break
610+
611+
if inode is None:
612+
raise FileNotFoundError(path)
613+
614+
cwd = inode
615+
616+
return cwd

ext4/volume.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,8 @@ def bad_blocks(self):
119119
return self.inodes[EXT4_INO.BAD]
120120

121121
@property
122-
def root(self):
123-
return self.inodes[EXT4_INO.ROOT]
122+
def root(self) -> Directory:
123+
return assert_cast(self.inodes[EXT4_INO.ROOT], Directory)
124124

125125
@property
126126
def user_quota(self):

0 commit comments

Comments
 (0)