ec2-byhand 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. diskread/__init__.py +23 -0
  2. diskread/blockdev.py +187 -0
  3. diskread/crc32c.py +72 -0
  4. diskread/errors.py +50 -0
  5. diskread/fs/__init__.py +328 -0
  6. diskread/fs/detect.py +75 -0
  7. diskread/fs/ext4/__init__.py +11 -0
  8. diskread/fs/ext4/blockmap.py +137 -0
  9. diskread/fs/ext4/csum.py +147 -0
  10. diskread/fs/ext4/fs.py +376 -0
  11. diskread/fs/ext4/inode.py +143 -0
  12. diskread/fs/ext4/superblock.py +284 -0
  13. diskread/fs/ext4/xattr.py +127 -0
  14. diskread/fs/extents.py +116 -0
  15. diskread/fs/fat/__init__.py +10 -0
  16. diskread/fs/fat/bpb.py +176 -0
  17. diskread/fs/fat/dirent.py +223 -0
  18. diskread/fs/fat/fs.py +283 -0
  19. diskread/fs/xfs/__init__.py +11 -0
  20. diskread/fs/xfs/attr.py +246 -0
  21. diskread/fs/xfs/bmap.py +119 -0
  22. diskread/fs/xfs/csum.py +32 -0
  23. diskread/fs/xfs/dir.py +112 -0
  24. diskread/fs/xfs/fs.py +322 -0
  25. diskread/fs/xfs/inode.py +157 -0
  26. diskread/fs/xfs/log.py +264 -0
  27. diskread/fs/xfs/superblock.py +215 -0
  28. diskread/lvm/__init__.py +27 -0
  29. diskread/lvm/config.py +158 -0
  30. diskread/lvm/label.py +208 -0
  31. diskread/lvm/vg.py +400 -0
  32. diskread/partition/__init__.py +114 -0
  33. diskread/partition/gpt.py +179 -0
  34. diskread/partition/mbr.py +142 -0
  35. diskread/py.typed +0 -0
  36. diskread/source.py +305 -0
  37. diskread/system.py +559 -0
  38. ec2_byhand/__init__.py +13 -0
  39. ec2_byhand/__main__.py +12 -0
  40. ec2_byhand/analyze.py +1918 -0
  41. ec2_byhand/archive.py +545 -0
  42. ec2_byhand/aws/__init__.py +13 -0
  43. ec2_byhand/aws/devices.py +64 -0
  44. ec2_byhand/aws/ebs.py +320 -0
  45. ec2_byhand/aws/snapshots.py +198 -0
  46. ec2_byhand/aws/upload.py +155 -0
  47. ec2_byhand/bolt.py +152 -0
  48. ec2_byhand/cli.py +920 -0
  49. ec2_byhand/cloudinit.py +352 -0
  50. ec2_byhand/containers.py +498 -0
  51. ec2_byhand/langpkgs.py +703 -0
  52. ec2_byhand/manifest.py +48 -0
  53. ec2_byhand/messages.py +640 -0
  54. ec2_byhand/noise.py +508 -0
  55. ec2_byhand/packages/__init__.py +145 -0
  56. ec2_byhand/packages/diff.py +94 -0
  57. ec2_byhand/packages/dpkg.py +178 -0
  58. ec2_byhand/packages/history.py +327 -0
  59. ec2_byhand/packages/model.py +87 -0
  60. ec2_byhand/packages/rpmdb.py +203 -0
  61. ec2_byhand/packages/rpmheader.py +178 -0
  62. ec2_byhand/packages/vercmp.py +195 -0
  63. ec2_byhand/packages/verify.py +496 -0
  64. ec2_byhand/presets.py +119 -0
  65. ec2_byhand/py.typed +0 -0
  66. ec2_byhand/report.py +265 -0
  67. ec2_byhand/secrets.py +204 -0
  68. ec2_byhand/sensitive.py +144 -0
  69. ec2_byhand/servicedirs.py +150 -0
  70. ec2_byhand/snaps.py +232 -0
  71. ec2_byhand-0.1.0.dist-info/METADATA +417 -0
  72. ec2_byhand-0.1.0.dist-info/RECORD +75 -0
  73. ec2_byhand-0.1.0.dist-info/WHEEL +4 -0
  74. ec2_byhand-0.1.0.dist-info/entry_points.txt +3 -0
  75. ec2_byhand-0.1.0.dist-info/licenses/LICENSE +21 -0
diskread/__init__.py ADDED
@@ -0,0 +1,23 @@
1
+ """diskread: pure-Python, read-only parsers for disks, partitions, LVM and filesystems.
2
+
3
+ This package must not depend on AWS or on ec2_byhand, so it can be tested
4
+ against local image files and split out later.
5
+
6
+ diskread: ディスク・パーティション・LVM・ファイルシステムを読み取り専用で解析する純 Python の
7
+ パーサー群。AWS にも ec2_byhand にも依存しない(ローカルのイメージファイルでテストでき、将来切り
8
+ 出せるように)。
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from diskread.blockdev import BlockDevice, BytesDevice, FileDevice, SliceDevice
14
+ from diskread.errors import DiskReadError, UnsupportedError
15
+
16
+ __all__ = [
17
+ "BlockDevice",
18
+ "BytesDevice",
19
+ "DiskReadError",
20
+ "FileDevice",
21
+ "SliceDevice",
22
+ "UnsupportedError",
23
+ ]
diskread/blockdev.py ADDED
@@ -0,0 +1,187 @@
1
+ """The "disk" abstraction every parser reads through.
2
+
3
+ A block device is just a fixed-size byte range with random access. Image
4
+ files, partitions, LVM logical volumes and EBS snapshots all implement it,
5
+ so parsers never care where the bytes come from.
6
+
7
+ 全パーサーが読み取りに使う「ディスク」の抽象。ブロックデバイスはランダムアクセスできる固定長の
8
+ バイト列にすぎず、イメージファイル・パーティション・LVM の論理ボリューム・EBS スナップショット
9
+ のどれも同じ形で扱える。
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import contextlib
15
+ import os
16
+ from collections.abc import Iterator
17
+ from pathlib import Path
18
+ from typing import BinaryIO, Protocol, runtime_checkable
19
+
20
+ from diskread.errors import DiskReadError
21
+
22
+
23
+ @runtime_checkable
24
+ class BlockDevice(Protocol):
25
+ """The read-only, random-access byte range every parser consumes.
26
+
27
+ 全パーサーが読む、読み取り専用でランダムアクセスできるバイト範囲。
28
+ """
29
+
30
+ @property
31
+ def size(self) -> int:
32
+ """Total size in bytes.
33
+
34
+ 全体のサイズ(バイト)。
35
+ """
36
+ ...
37
+
38
+ def read_at(self, offset: int, length: int) -> bytes:
39
+ """Read exactly `length` bytes at `offset`.
40
+
41
+ Raises DiskReadError if the range extends past the end of the device.
42
+
43
+ `offset` から `length` バイトちょうどを読む。範囲がデバイスの端を越えると
44
+ DiskReadError。
45
+ """
46
+ ...
47
+
48
+
49
+ @runtime_checkable
50
+ class ReadAhead(Protocol):
51
+ """A block device that can start fetching what it is told will be read next.
52
+
53
+ 次に読まれると告げられた範囲を、先に取り寄せ始められるブロックデバイス。
54
+ """
55
+
56
+ def will_read(self, offset: int, length: int) -> None:
57
+ """Note that [offset, offset + length) is to be read, after what was noted before.
58
+
59
+ [offset, offset + length) を、これまでに告げた範囲の後に読む予定だと伝える。
60
+ """
61
+ ...
62
+
63
+
64
+ def will_read(dev: BlockDevice, offset: int = 0, length: int | None = None) -> None:
65
+ """Tell `dev` that [offset, offset + length) (by default, to the end) is to be read
66
+ next, after the ranges told before, so that a device that reads over a network can
67
+ fetch it ahead. Only a hint: nothing is read here, devices that cannot fetch ahead
68
+ ignore it, and it never fails: a range the device does not have (damaged metadata
69
+ that points past its end) is left to the read, which meets the error and reports
70
+ it. Tell only what will be read, since what a device fetches ahead and nobody reads
71
+ is fetched for nothing.
72
+
73
+ `dev` に [offset, offset + length)(既定は末尾まで)を、先に告げた範囲の次に読むと伝え、
74
+ ネットワーク越しに読むデバイスが先読みできるようにする。ヒントにすぎず、ここでは何も読まな
75
+ い。先読みできないデバイスは無視する。失敗もしない: デバイスに無い範囲(壊れたメタデータが
76
+ 末尾の先を指している)は読み出しに任せ、そこでエラーになって報告される。先読みして誰も読ま
77
+ なければ無駄になるので、本当に読む範囲だけを告げる。
78
+ """
79
+ if isinstance(dev, ReadAhead):
80
+ with contextlib.suppress(DiskReadError):
81
+ dev.will_read(offset, dev.size - offset if length is None else length)
82
+
83
+
84
+ def check_range(dev_size: int, offset: int, length: int) -> None:
85
+ """Raise DiskReadError unless [offset, offset + length) fits in dev_size.
86
+
87
+ [offset, offset + length) が dev_size に収まらなければ DiskReadError。
88
+ """
89
+ if offset < 0 or length < 0 or offset + length > dev_size:
90
+ raise DiskReadError(f"read out of range: offset={offset} length={length} size={dev_size}")
91
+
92
+
93
+ def read_chunks(dev: BlockDevice, chunk_size: int = 1 << 20) -> Iterator[bytes]:
94
+ """The device's bytes in order, a chunk at a time, for files too large to hold in memory.
95
+
96
+ デバイスのバイト列を先頭から少しずつ返す(メモリに載せきれない大きなファイルのため)。
97
+ """
98
+ for offset in range(0, dev.size, chunk_size):
99
+ yield dev.read_at(offset, min(chunk_size, dev.size - offset))
100
+
101
+
102
+ class FileDevice:
103
+ """A block device backed by a local image file.
104
+
105
+ ローカルのイメージファイルを背にしたブロックデバイス。
106
+ """
107
+
108
+ def __init__(self, path: str | os.PathLike[str]) -> None:
109
+ # Stays open until close() (or __exit__), so no `with` here.
110
+ self._f: BinaryIO = Path(path).open("rb") # noqa: SIM115
111
+ self._size = os.fstat(self._f.fileno()).st_size
112
+
113
+ @property
114
+ def size(self) -> int:
115
+ return self._size
116
+
117
+ def read_at(self, offset: int, length: int) -> bytes:
118
+ check_range(self._size, offset, length)
119
+ self._f.seek(offset)
120
+ data = self._f.read(length)
121
+ if len(data) != length:
122
+ raise DiskReadError(f"short read at offset {offset}")
123
+ return data
124
+
125
+ def close(self) -> None:
126
+ """Close the underlying image file.
127
+
128
+ 下のイメージファイルを閉じる。
129
+ """
130
+ self._f.close()
131
+
132
+ def __enter__(self) -> FileDevice:
133
+ return self
134
+
135
+ def __exit__(self, *exc: object) -> None:
136
+ self.close()
137
+
138
+
139
+ class BytesDevice:
140
+ """A block device held in memory. Mostly useful for tests.
141
+
142
+ メモリ上のブロックデバイス。主にテスト用。
143
+ """
144
+
145
+ def __init__(self, data: bytes | bytearray) -> None:
146
+ self._data = bytes(data)
147
+
148
+ @property
149
+ def size(self) -> int:
150
+ return len(self._data)
151
+
152
+ def read_at(self, offset: int, length: int) -> bytes:
153
+ check_range(len(self._data), offset, length)
154
+ return self._data[offset : offset + length]
155
+
156
+
157
+ class SliceDevice:
158
+ """A sub-range of another block device, e.g. a partition.
159
+
160
+ 別のブロックデバイスの部分範囲。パーティションなど。
161
+ """
162
+
163
+ def __init__(self, parent: BlockDevice, offset: int, size: int) -> None:
164
+ check_range(parent.size, offset, size)
165
+ self._parent = parent
166
+ self._offset = offset
167
+ self._size = size
168
+
169
+ @property
170
+ def size(self) -> int:
171
+ return self._size
172
+
173
+ @property
174
+ def offset(self) -> int:
175
+ """Start of this slice within the parent device, in bytes.
176
+
177
+ 親デバイス内でのこのスライスの開始位置(バイト)。
178
+ """
179
+ return self._offset
180
+
181
+ def read_at(self, offset: int, length: int) -> bytes:
182
+ check_range(self._size, offset, length)
183
+ return self._parent.read_at(self._offset + offset, length)
184
+
185
+ def will_read(self, offset: int, length: int) -> None:
186
+ check_range(self._size, offset, length) # not what lies beside it on the parent
187
+ will_read(self._parent, self._offset + offset, length)
diskread/crc32c.py ADDED
@@ -0,0 +1,72 @@
1
+ """CRC-32C (Castagnoli), as used by ext4 metadata_csum and XFS v5.
2
+
3
+ `crc32c_raw` is the bare register update with no pre/post inversion, like
4
+ the kernel's crc32c_le(): ext4 chains it and stores the raw value. XFS
5
+ starts from ~0 and stores the inverted result, which is the standard
6
+ CRC-32C that `crc32c` returns.
7
+
8
+ Pure Python, so the inner loop matters: this is slicing-by-8 (8 lookup
9
+ tables, one struct-decoded 8-byte step per iteration), several times
10
+ faster than the classic one-table byte loop.
11
+
12
+ ext4 metadata_csum と XFS v5 が使う CRC-32C(Castagnoli)。`crc32c_raw` は前後の反転なしの素の
13
+ 更新(カーネルの crc32c_le() 相当)で、`crc32c` は標準の CRC-32C。純 Python なので内側のループ
14
+ が要で、slicing-by-8 により古典的な 1 テーブル方式の数倍速い。
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import struct
20
+
21
+ _POLY = 0x82F63B78 # reflected 0x1EDC6F41
22
+
23
+
24
+ def _tables() -> list[list[int]]:
25
+ """Build the eight slicing-by-8 lookup tables."""
26
+ t0 = []
27
+ for i in range(256):
28
+ c = i
29
+ for _ in range(8):
30
+ c = (c >> 1) ^ _POLY if c & 1 else c >> 1
31
+ t0.append(c)
32
+ tables = [t0]
33
+ for _ in range(7):
34
+ prev = tables[-1]
35
+ tables.append([t0[c & 0xFF] ^ (c >> 8) for c in prev])
36
+ return tables
37
+
38
+
39
+ _T0, _T1, _T2, _T3, _T4, _T5, _T6, _T7 = _tables()
40
+
41
+
42
+ def crc32c_raw(crc: int, data: bytes) -> int:
43
+ """Advance the bare CRC register over `data` (no pre/post inversion).
44
+
45
+ 前後の反転なしの素の CRC レジスタを `data` のぶん進める。
46
+ """
47
+ t0, t1, t2, t3 = _T0, _T1, _T2, _T3
48
+ t4, t5, t6, t7 = _T4, _T5, _T6, _T7
49
+ head = len(data) & ~7
50
+ for lo, hi in struct.iter_unpack("<II", data[:head]):
51
+ lo ^= crc
52
+ crc = (
53
+ t7[lo & 0xFF]
54
+ ^ t6[(lo >> 8) & 0xFF]
55
+ ^ t5[(lo >> 16) & 0xFF]
56
+ ^ t4[lo >> 24]
57
+ ^ t3[hi & 0xFF]
58
+ ^ t2[(hi >> 8) & 0xFF]
59
+ ^ t1[(hi >> 16) & 0xFF]
60
+ ^ t0[hi >> 24]
61
+ )
62
+ for b in data[head:]:
63
+ crc = t0[(crc ^ b) & 0xFF] ^ (crc >> 8)
64
+ return crc
65
+
66
+
67
+ def crc32c(data: bytes) -> int:
68
+ """The standard CRC-32C of `data`.
69
+
70
+ `data` の標準の CRC-32C。
71
+ """
72
+ return crc32c_raw(0xFFFFFFFF, data) ^ 0xFFFFFFFF
diskread/errors.py ADDED
@@ -0,0 +1,50 @@
1
+ """Errors every diskread parser raises.
2
+
3
+ diskread の全パーサーが投げるエラー。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import struct
9
+ from collections.abc import Callable
10
+ from functools import wraps
11
+
12
+ # What parsing truncated or nonsensical bytes raises when no explicit check
13
+ # caught it first: short struct.unpack_from, indexing past the end, bad UTF-8,
14
+ # dividing by a size that reads as zero (ArithmeticError, with OverflowError)...
15
+ MALFORMED_DATA_ERRORS = (struct.error, LookupError, ValueError, ArithmeticError, RecursionError)
16
+
17
+
18
+ class DiskReadError(Exception):
19
+ """The on-disk data is malformed, truncated, or fails a checksum.
20
+
21
+ ディスク上のデータが壊れている・切れている・チェックサムが合わない。
22
+ """
23
+
24
+
25
+ class UnsupportedError(DiskReadError):
26
+ """The on-disk data is valid but uses a feature diskread does not handle.
27
+
28
+ データ自体は正しいが、diskread が扱わない機能を使っている。
29
+ """
30
+
31
+
32
+ def malformed_as_disk_read_error[**P, R](func: Callable[P, R]) -> Callable[P, R]:
33
+ """Make `func`, which parses on-disk bytes, report malformed data as DiskReadError.
34
+
35
+ Callers only handle DiskReadError, so that one bad inode costs one
36
+ warning. Every check a parser could miss must not become a traceback.
37
+
38
+ ディスク上のバイト列を解析する `func` が、壊れたデータを DiskReadError として報告するように
39
+ する。呼び出し側は DiskReadError だけを扱うので、inode が 1 つ壊れていても警告 1 件で済む。
40
+ パーサーの検査漏れがトレースバックにならないようにするためのもの。
41
+ """
42
+
43
+ @wraps(func)
44
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
45
+ try:
46
+ return func(*args, **kwargs)
47
+ except MALFORMED_DATA_ERRORS as err:
48
+ raise DiskReadError(f"malformed on-disk data: {err!r}") from err
49
+
50
+ return wrapper
@@ -0,0 +1,328 @@
1
+ """Filesystem-independent types and helpers shared by the ext4, XFS and FAT readers.
2
+
3
+ 各ファイルシステムのリーダーが共有する、ファイルシステム非依存の型とヘルパー。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import contextlib
9
+ import stat as _stat
10
+ from collections.abc import Callable, Iterator
11
+ from dataclasses import dataclass, field
12
+ from enum import StrEnum
13
+ from typing import Protocol, runtime_checkable
14
+
15
+ from diskread.blockdev import BlockDevice
16
+ from diskread.errors import DiskReadError, malformed_as_disk_read_error
17
+
18
+
19
+ class FileType(StrEnum):
20
+ """An inode's type as a short lowercase name ("reg", "dir", ...).
21
+
22
+ inode の種別(「reg」「dir」などの短い小文字名)。
23
+ """
24
+
25
+ REG = "reg"
26
+ DIR = "dir"
27
+ LNK = "lnk"
28
+ CHR = "chr"
29
+ BLK = "blk"
30
+ FIFO = "fifo"
31
+ SOCK = "sock"
32
+
33
+
34
+ _TYPES = {
35
+ _stat.S_IFREG: FileType.REG,
36
+ _stat.S_IFDIR: FileType.DIR,
37
+ _stat.S_IFLNK: FileType.LNK,
38
+ _stat.S_IFCHR: FileType.CHR,
39
+ _stat.S_IFBLK: FileType.BLK,
40
+ _stat.S_IFIFO: FileType.FIFO,
41
+ _stat.S_IFSOCK: FileType.SOCK,
42
+ }
43
+
44
+
45
+ def file_type(mode: int) -> FileType:
46
+ """The FileType in `mode`'s S_IFMT bits; DiskReadError if unknown.
47
+
48
+ `mode` の S_IFMT ビットが表す FileType。未知なら DiskReadError。
49
+ """
50
+ try:
51
+ return _TYPES[_stat.S_IFMT(mode)]
52
+ except KeyError:
53
+ raise DiskReadError(f"unknown file type in mode {mode:#o}") from None
54
+
55
+
56
+ @dataclass(frozen=True, slots=True) # slots: one is kept for every path that is walked
57
+ class Stat:
58
+ """What stat(2) would report for an inode, as Linux computes it.
59
+
60
+ その inode について stat(2) が返す内容(Linux の計算に合わせる)。
61
+ """
62
+
63
+ ino: int
64
+ mode: int # including the S_IFMT type bits
65
+ uid: int
66
+ gid: int
67
+ size: int
68
+ nlink: int
69
+ blocks: int # in 512-byte units, like st_blocks
70
+ atime_ns: int
71
+ mtime_ns: int
72
+ ctime_ns: int
73
+ btime_ns: int | None # None when the filesystem does not record it
74
+ rdev: tuple[int, int] | None # (major, minor) for device nodes
75
+ _type: FileType | None = field(default=None, init=False, repr=False, compare=False)
76
+
77
+ @property
78
+ def type(self) -> FileType:
79
+ """The FileType encoded in `mode`. Asked for on every path step of every lookup:
80
+ worked out once and kept.
81
+
82
+ `mode` が表す FileType。パス解決の各段で毎回聞かれるので、1 度求めて覚えておく。
83
+ """
84
+ found = self._type
85
+ if found is None:
86
+ found = file_type(self.mode)
87
+ object.__setattr__(self, "_type", found) # frozen: this is only a memo
88
+ return found
89
+
90
+
91
+ class Filesystem(Protocol):
92
+ """What every filesystem reader provides; walk() and lookup() build on it.
93
+
94
+ 各ファイルシステムリーダーが提供する読み取り口。walk() と lookup() はこの上に載る。
95
+ """
96
+
97
+ fs_type: str # "ext4" (also for ext2 / ext3), "xfs" or "vfat", as detect_fs names them
98
+
99
+ @property
100
+ def is_dirty(self) -> bool:
101
+ """Whether it may hold changes only in its journal or log, which are not replayed.
102
+
103
+ 変更がジャーナルやログにしか無い可能性があるか(それらは再生しない)。
104
+ """
105
+ ...
106
+
107
+ @property
108
+ def root_ino(self) -> int:
109
+ """The root directory's inode number.
110
+
111
+ ルートディレクトリの inode 番号。
112
+ """
113
+ ...
114
+
115
+ def stat(self, ino: int) -> Stat:
116
+ """What stat(2) would report for the inode.
117
+
118
+ その inode の stat(2) 相当の情報。
119
+ """
120
+ ...
121
+
122
+ def listdir(self, ino: int) -> list[tuple[bytes, int]]:
123
+ """Directory entries as (name, inode), without "." and "..".
124
+
125
+ ディレクトリの (名前, inode) の一覧。「.」と「..」は含まない。
126
+ """
127
+ ...
128
+
129
+ def open(self, ino: int) -> BlockDevice:
130
+ """The contents of a regular file (holes read as zeros).
131
+
132
+ 通常ファイルの内容(穴はゼロとして読める)。
133
+ """
134
+ ...
135
+
136
+ def readlink(self, ino: int) -> bytes:
137
+ """The symlink's target, as raw bytes.
138
+
139
+ シンボリックリンクのリンク先(生のバイト列)。
140
+ """
141
+ ...
142
+
143
+ def xattrs(self, ino: int) -> dict[str, bytes]:
144
+ """Extended attributes as getxattr(2) would return them.
145
+
146
+ getxattr(2) が返す形の拡張属性。
147
+ """
148
+ ...
149
+
150
+
151
+ def reads_disk_metadata[T](cls: type[T]) -> type[T]:
152
+ """Class decorator for a filesystem reader: whatever its Filesystem methods
153
+ make of malformed metadata comes out as DiskReadError.
154
+
155
+ ファイルシステムリーダーのクラスデコレータ: Filesystem のメソッドが壊れたメタデータで何を起
156
+ こしても DiskReadError として出す。
157
+ """
158
+ # will_list too, where a reader has it: it parses the same metadata as listdir.
159
+ for name in ("stat", "listdir", "open", "readlink", "xattrs", "will_list"):
160
+ if hasattr(cls, name):
161
+ setattr(cls, name, malformed_as_disk_read_error(getattr(cls, name)))
162
+ return cls
163
+
164
+
165
+ def valid_name(name: bytes) -> bool:
166
+ """Whether a directory entry's name is one a path can hold: not empty, without "/"
167
+ or NUL, and not "." or "..". A disk can hold any bytes; a name that is none would
168
+ lead a path somewhere else than under its directory.
169
+
170
+ ディレクトリエントリの名前が、パスに入れられるものか: 空でなく、「/」も NUL も含まず、
171
+ 「.」「..」でもない。ディスクにはどんなバイト列もあり得て、名前でないものを繋ぐと、パスが
172
+ そのディレクトリの下ではない場所を指してしまう。
173
+ """
174
+ return bool(name) and b"/" not in name and b"\0" not in name and name not in (b".", b"..")
175
+
176
+
177
+ INODE_CACHE = 1 << 18 # inodes a filesystem keeps the bytes of (262,144)
178
+ PARSED_INODES = 1 << 13 # and those it keeps parsed too (8,192): the ones in use
179
+
180
+
181
+ @runtime_checkable
182
+ class ListsAhead(Protocol):
183
+ """A filesystem that can tell its disk where a directory's entries are.
184
+
185
+ ディレクトリの中身がどこにあるかをディスクに告げられるファイルシステム。
186
+ """
187
+
188
+ def will_list(self, ino: int) -> None:
189
+ """Tell the disk (will_read) the blocks listdir(ino) will read, nothing more.
190
+
191
+ listdir(ino) が読むブロックだけをディスクに告げる(will_read)。
192
+ """
193
+ ...
194
+
195
+
196
+ def will_list(fs: Filesystem, ino: int) -> None:
197
+ """Tell the disk under `fs` where directory `ino`'s entries are, when it can say:
198
+ a hint only, nothing is read but the directory's own metadata, and an error is
199
+ left to listdir, which meets it again.
200
+
201
+ `fs` の下のディスクに、ディレクトリ `ino` の中身の位置を告げる(告げられる場合)。ヒント
202
+ にすぎず、読むのはディレクトリ自身のメタデータだけ。エラーは listdir に任せる。
203
+ """
204
+ if isinstance(fs, ListsAhead):
205
+ with contextlib.suppress(DiskReadError):
206
+ fs.will_list(ino)
207
+
208
+
209
+ def walk(
210
+ fs: Filesystem,
211
+ on_error: Callable[[bytes, DiskReadError], None] | None = None,
212
+ prune: Callable[[bytes], bool] | None = None,
213
+ listdir: Callable[[int], list[tuple[bytes, int]]] | None = None,
214
+ stat: Callable[[int], Stat] | None = None,
215
+ tell: Callable[[int], None] | None = None,
216
+ ) -> Iterator[tuple[bytes, int, Stat]]:
217
+ """Yield (path, inode, stat) for every reachable file, root first, a level of
218
+ directories at a time.
219
+
220
+ Paths are absolute byte strings ("/", "/etc", "/etc/passwd"). Each
221
+ directory is entered once even if corrupt metadata creates a cycle.
222
+
223
+ By default the first DiskReadError aborts the walk. With `on_error`,
224
+ the error is reported with the path it concerns and that entry (or
225
+ that directory's contents) is skipped, so one bad inode in a snapshot
226
+ of a live system does not hide the rest of the tree.
227
+
228
+ Entries whose path satisfies `prune` are neither yielded nor entered
229
+ (used to hide what lies under a mountpoint). `listdir` and `stat`
230
+ replace the filesystem's own calls, so a caller can route them through
231
+ its caches (as SystemSource.walk does, to avoid re-reading the same
232
+ metadata when it resolves paths after the walk).
233
+
234
+ Breadth first: every directory of a level is told to the disk (`tell`, by
235
+ default will_list) before any is listed, so that a disk read over a network
236
+ fetches them all at once. Depth first, the next directory is known only once
237
+ the one before is read, and each waited for its blocks (nine tenths of what an
238
+ analysis over the EBS direct APIs waited for). With `listdir` given, `tell` is
239
+ the caller's: only it knows which directories its listdir will read.
240
+
241
+ 到達できる全ファイルを (path, inode, stat) で、ルートから順に yield する。パスは絶対のバイ
242
+ ト列。既定では最初の DiskReadError で中断し、`on_error` を渡すと該当パスを添えて報告してそ
243
+ の項目(またはそのディレクトリの中身)だけを飛ばす。`prune` に合うパスは出さず、中にも入ら
244
+ ない。`listdir` と `stat` を渡すとファイルシステム直呼びの代わりに使う(SystemSource.walk
245
+ が自分のキャッシュ経由にして、walk 後のパス解決で同じメタデータを読み直さないため)。
246
+ """
247
+
248
+ def fail(path: bytes, err: DiskReadError) -> None:
249
+ if on_error is None:
250
+ raise err
251
+ on_error(path, err)
252
+
253
+ if tell is None and listdir is None:
254
+ tell = lambda ino: will_list(fs, ino) # noqa: E731
255
+ listdir = fs.listdir if listdir is None else listdir
256
+ stat = fs.stat if stat is None else stat
257
+ root = fs.root_ino
258
+ try:
259
+ root_stat = stat(root)
260
+ except DiskReadError as err: # a volume whose root is damaged: nothing of it, said once
261
+ fail(b"/", err)
262
+ return
263
+ yield b"/", root, root_stat
264
+ seen_dirs = {root}
265
+ level: list[tuple[bytes, int]] = [(b"", root)]
266
+ while level:
267
+ if tell is not None:
268
+ for _, dir_ino in level:
269
+ tell(dir_ino)
270
+ below: list[tuple[bytes, int]] = []
271
+ for prefix, dir_ino in level:
272
+ try:
273
+ entries = listdir(dir_ino)
274
+ except DiskReadError as err:
275
+ fail(prefix or b"/", err)
276
+ continue
277
+ for name, ino in entries:
278
+ path = prefix + b"/" + name
279
+ if not valid_name(name):
280
+ fail(path, DiskReadError(f"invalid name {name!r} in directory inode {dir_ino}"))
281
+ continue
282
+ if prune is not None and prune(path):
283
+ continue
284
+ try:
285
+ st = stat(ino)
286
+ except DiskReadError as err:
287
+ fail(path, err)
288
+ continue
289
+ if st.type is FileType.DIR and ino in seen_dirs:
290
+ fail(path, DiskReadError(f"directory inode {ino} reached twice"))
291
+ continue
292
+ yield path, ino, st
293
+ if st.type is FileType.DIR:
294
+ seen_dirs.add(ino)
295
+ below.append((path, ino))
296
+ level = below
297
+
298
+
299
+ def lookup(fs: Filesystem, path: bytes | str) -> int:
300
+ """Resolve an absolute path to an inode without following symlinks.
301
+
302
+ 絶対パスを、シンボリックリンクを辿らずに inode に解決する。
303
+ """
304
+ if isinstance(path, str):
305
+ path = path.encode("utf-8", "surrogateescape")
306
+ ino = fs.root_ino
307
+ for part in path.split(b"/"):
308
+ if not part:
309
+ continue
310
+ for name, child in fs.listdir(ino):
311
+ if name == part:
312
+ ino = child
313
+ break
314
+ else:
315
+ raise FileNotFoundError(path)
316
+ return ino
317
+
318
+
319
+ __all__ = [
320
+ "FileType",
321
+ "Filesystem",
322
+ "Stat",
323
+ "file_type",
324
+ "lookup",
325
+ "reads_disk_metadata",
326
+ "valid_name",
327
+ "walk",
328
+ ]