humanish 1.0.0__tar.gz

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.
@@ -0,0 +1,20 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ .venv/
9
+ venv/
10
+
11
+ # Tooling
12
+ .pytest_cache/
13
+ .ruff_cache/
14
+ .mypy_cache/
15
+
16
+ # Editors / OS
17
+ .vscode/
18
+ .idea/
19
+ .DS_Store
20
+ Thumbs.db
humanish-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hunter Hall
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,67 @@
1
+ Metadata-Version: 2.5
2
+ Name: humanish
3
+ Version: 1.0.0
4
+ Summary: Detect a file's real format from its bytes and read a human-readable summary of its contents
5
+ Project-URL: Homepage, https://github.com/hunterhall/humanish
6
+ Project-URL: Repository, https://github.com/hunterhall/humanish
7
+ Author-email: Hunter Hall <hunter.hall.hgh@gmail.com>
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: detection,file-format,file-type,magic-bytes,metadata
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Utilities
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+
18
+ # humanish
19
+
20
+ Detect a file's real format from its magic bytes (not its extension) and get a
21
+ human-readable summary of what's inside it.
22
+
23
+ Supports PNG, JPEG, BMP, WAV, MP3, ZIP, TAR, GZIP, PDF, PE (EXE/DLL), ELF, and
24
+ MP4. Pure standard library, no dependencies.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pip install git+https://github.com/hunterhall/humanish.git
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ```python
35
+ from humanish import humanish, detect_format
36
+
37
+ detect_format("mystery.bin") # -> "png"
38
+
39
+ humanish("photo.jpg")
40
+ # {
41
+ # "format": "jpeg",
42
+ # "width": 1920,
43
+ # "height": 1080,
44
+ # "components": 3,
45
+ # ...
46
+ # }
47
+ ```
48
+
49
+ Detect from bytes you already have in memory:
50
+
51
+ ```python
52
+ from humanish import detect_format_from_bytes
53
+
54
+ detect_format_from_bytes(open("mystery.bin", "rb").read(64))
55
+ ```
56
+
57
+ ## Command line
58
+
59
+ ```bash
60
+ humanish path/to/file
61
+ ```
62
+
63
+ Prints the format and content summary as JSON.
64
+
65
+ ## License
66
+
67
+ MIT
@@ -0,0 +1,50 @@
1
+ # humanish
2
+
3
+ Detect a file's real format from its magic bytes (not its extension) and get a
4
+ human-readable summary of what's inside it.
5
+
6
+ Supports PNG, JPEG, BMP, WAV, MP3, ZIP, TAR, GZIP, PDF, PE (EXE/DLL), ELF, and
7
+ MP4. Pure standard library, no dependencies.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install git+https://github.com/hunterhall/humanish.git
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```python
18
+ from humanish import humanish, detect_format
19
+
20
+ detect_format("mystery.bin") # -> "png"
21
+
22
+ humanish("photo.jpg")
23
+ # {
24
+ # "format": "jpeg",
25
+ # "width": 1920,
26
+ # "height": 1080,
27
+ # "components": 3,
28
+ # ...
29
+ # }
30
+ ```
31
+
32
+ Detect from bytes you already have in memory:
33
+
34
+ ```python
35
+ from humanish import detect_format_from_bytes
36
+
37
+ detect_format_from_bytes(open("mystery.bin", "rb").read(64))
38
+ ```
39
+
40
+ ## Command line
41
+
42
+ ```bash
43
+ humanish path/to/file
44
+ ```
45
+
46
+ Prints the format and content summary as JSON.
47
+
48
+ ## License
49
+
50
+ MIT
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "humanish"
7
+ version = "1.0.0"
8
+ description = "Detect a file's real format from its bytes and read a human-readable summary of its contents"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Hunter Hall", email = "hunter.hall.hgh@gmail.com" }]
13
+ keywords = ["file-format", "magic-bytes", "file-type", "detection", "metadata"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ "Topic :: Utilities",
19
+ ]
20
+ dependencies = []
21
+
22
+ [project.urls]
23
+ Homepage = "https://github.com/hunterhall/humanish"
24
+ Repository = "https://github.com/hunterhall/humanish"
25
+
26
+ [project.scripts]
27
+ humanish = "humanish.humanish:main"
28
+
29
+ [tool.hatch.build.targets.wheel]
30
+ packages = ["src/humanish"]
@@ -0,0 +1,16 @@
1
+ from humanish.check_format import (
2
+ detect_format,
3
+ detect_format_from_bytes,
4
+ )
5
+ from humanish.humanish import humanish
6
+ from humanish.read import READERS
7
+
8
+ __version__ = "0.1.0"
9
+
10
+ __all__ = [
11
+ "humanish",
12
+ "detect_format",
13
+ "detect_format_from_bytes",
14
+ "READERS",
15
+ "__version__",
16
+ ]
@@ -0,0 +1,108 @@
1
+ from pathlib import Path
2
+
3
+ _SIGNATURES = [
4
+ (b"\x89PNG\r\n\x1a\n", 0, "png"),
5
+ (b"\xFF\xD8\xFF", 0, "jpeg"),
6
+ (b"BM", 0, "bmp"),
7
+ (b"ID3", 0, "mp3"),
8
+ (b"PK\x03\x04", 0, "zip"),
9
+ (b"PK\x05\x06", 0, "zip"),
10
+ (b"\x1F\x8B", 0, "gzip"),
11
+ (b"%PDF-", 0, "pdf"),
12
+ (b"MZ", 0, "pe"),
13
+ (b"\x7fELF", 0, "elf"),
14
+ (b"ustar", 257, "tar"),
15
+ ]
16
+
17
+
18
+ def _matches_mp3_frame_sync(raw: bytes) -> bool:
19
+ return len(raw) >= 2 and raw[0] == 0xFF and (raw[1] & 0xE0) == 0xE0
20
+
21
+
22
+ def _matches_riff(raw: bytes) -> str | None:
23
+ if len(raw) < 12 or raw[0:4] != b"RIFF":
24
+ return None
25
+ form_type = raw[8:12]
26
+ if form_type == b"WAVE":
27
+ return "wav"
28
+ return "riff"
29
+
30
+
31
+ def _matches_mp4(raw: bytes) -> bool:
32
+ return len(raw) >= 12 and raw[4:8] == b"ftyp"
33
+
34
+
35
+ def _classify_pe(raw: bytes) -> str:
36
+ DLL_CHARACTERISTIC_FLAG = 0x2000
37
+ try:
38
+ pe_header_offset = int.from_bytes(raw[0x3C:0x40], "little")
39
+ coff_header = raw[pe_header_offset + 4: pe_header_offset + 24]
40
+ characteristics = int.from_bytes(coff_header[18:20], "little")
41
+ return "dll" if characteristics & DLL_CHARACTERISTIC_FLAG else "exe"
42
+ except IndexError:
43
+ return "pe"
44
+
45
+
46
+ def detect_format(path: str) -> str:
47
+ raw = Path(path).read_bytes()
48
+
49
+ if _matches_mp4(raw):
50
+ return "mp4"
51
+
52
+ riff_type = _matches_riff(raw)
53
+ if riff_type == "wav":
54
+ return "wav"
55
+
56
+ for signature, offset, name in _SIGNATURES:
57
+ if raw[offset:offset + len(signature)] == signature:
58
+ if name == "pe":
59
+ return _classify_pe(raw)
60
+ return name
61
+
62
+ if _matches_mp3_frame_sync(raw):
63
+ return "mp3"
64
+
65
+ return "unknown"
66
+
67
+
68
+ def detect_format_from_bytes(raw: bytes) -> str:
69
+ if _matches_mp4(raw):
70
+ return "mp4"
71
+
72
+ riff_type = _matches_riff(raw)
73
+ if riff_type == "wav":
74
+ return "wav"
75
+
76
+ for signature, offset, name in _SIGNATURES:
77
+ if raw[offset:offset + len(signature)] == signature:
78
+ if name == "pe":
79
+ return _classify_pe(raw)
80
+ return name
81
+
82
+ if _matches_mp3_frame_sync(raw):
83
+ return "mp3"
84
+
85
+ return "unknown"
86
+
87
+
88
+ def identify_and_read(path: str) -> dict:
89
+ from humanish.read import READERS
90
+
91
+ fmt = detect_format(path)
92
+ reader = READERS.get(fmt)
93
+ if reader is None:
94
+ return {"format": fmt, "error": "no reader available for this format"}
95
+
96
+ result = reader(path)
97
+ result["format"] = fmt
98
+ return result
99
+
100
+
101
+ if __name__ == "__main__":
102
+ import sys
103
+
104
+ if len(sys.argv) != 2:
105
+ print("Usage: python -m humanish.check_format <path>")
106
+ sys.exit(1)
107
+
108
+ print(detect_format(sys.argv[1]))
@@ -0,0 +1,28 @@
1
+ from humanish.check_format import detect_format
2
+ from humanish.read import READERS
3
+
4
+
5
+ def humanish(path: str) -> dict:
6
+ fmt = detect_format(path)
7
+ reader = READERS.get(fmt)
8
+ if reader is None:
9
+ return {"format": fmt, "error": "no reader available for this format"}
10
+
11
+ result = reader(path)
12
+ result["format"] = fmt
13
+ return result
14
+
15
+
16
+ def main() -> None:
17
+ import json
18
+ import sys
19
+
20
+ if len(sys.argv) != 2:
21
+ print("Usage: humanish <path>", file=sys.stderr)
22
+ sys.exit(1)
23
+
24
+ print(json.dumps(humanish(sys.argv[1]), indent=2, default=str))
25
+
26
+
27
+ if __name__ == "__main__":
28
+ main()
@@ -0,0 +1,451 @@
1
+ import gzip
2
+ import re
3
+ import tarfile
4
+ import wave
5
+ import zipfile
6
+ from pathlib import Path
7
+
8
+
9
+ def read_png(path: str) -> dict:
10
+ color_type_names = {
11
+ 0: "grayscale",
12
+ 2: "rgb",
13
+ 3: "palette",
14
+ 4: "grayscale+alpha",
15
+ 6: "rgba",
16
+ }
17
+
18
+ raw = Path(path).read_bytes()
19
+
20
+ image_info = None
21
+ chunk_list = []
22
+
23
+ position = 8
24
+ while position + 8 <= len(raw):
25
+ chunk_length = int.from_bytes(raw[position:position + 4], "big")
26
+ chunk_type = raw[position + 4:position + 8].decode("ascii", errors="replace")
27
+ chunk_data = raw[position + 8: position + 8 + chunk_length]
28
+
29
+ chunk_list.append({
30
+ "type": chunk_type,
31
+ "length": chunk_length,
32
+ "offset": position,
33
+ })
34
+
35
+ if chunk_type == "IHDR":
36
+ image_info = {
37
+ "width": int.from_bytes(chunk_data[0:4], "big"),
38
+ "height": int.from_bytes(chunk_data[4:8], "big"),
39
+ "bit_depth": chunk_data[8],
40
+ "color_type": color_type_names.get(chunk_data[9], chunk_data[9]),
41
+ "interlaced": bool(chunk_data[12]),
42
+ }
43
+
44
+ position += 4 + 4 + chunk_length + 4
45
+
46
+ if chunk_type == "IEND":
47
+ break
48
+
49
+ return {
50
+ "image_info": image_info,
51
+ "chunks": chunk_list,
52
+ }
53
+
54
+
55
+ def read_jpeg(path: str) -> dict:
56
+ raw = Path(path).read_bytes()
57
+ if raw[:2] != b"\xFF\xD8":
58
+ return {"error": "not a JPEG file"}
59
+
60
+ width = height = components = None
61
+ markers = []
62
+
63
+ position = 2
64
+ while position < len(raw) - 1:
65
+ if raw[position] != 0xFF:
66
+ position += 1
67
+ continue
68
+
69
+ marker = raw[position + 1]
70
+
71
+ if marker in (0xD8, 0xD9, 0x01) or 0xD0 <= marker <= 0xD7:
72
+ position += 2
73
+ continue
74
+
75
+ if position + 4 > len(raw):
76
+ break
77
+
78
+ segment_length = int.from_bytes(raw[position + 2:position + 4], "big")
79
+ markers.append({"marker": f"0x{marker:02X}", "offset": position, "length": segment_length})
80
+
81
+ if marker in (0xC0, 0xC1, 0xC2, 0xC3):
82
+ height = int.from_bytes(raw[position + 5:position + 7], "big")
83
+ width = int.from_bytes(raw[position + 7:position + 9], "big")
84
+ components = raw[position + 9]
85
+
86
+ if marker == 0xDA:
87
+ break
88
+
89
+ position += 2 + segment_length
90
+
91
+ return {
92
+ "width": width,
93
+ "height": height,
94
+ "components": components,
95
+ "markers": markers,
96
+ "byte_size": len(raw),
97
+ }
98
+
99
+
100
+ def read_bmp(path: str) -> dict:
101
+ raw = Path(path).read_bytes()
102
+ if raw[:2] != b"BM":
103
+ return {"error": "not a BMP file"}
104
+
105
+ file_size = int.from_bytes(raw[2:6], "little")
106
+ pixel_data_offset = int.from_bytes(raw[10:14], "little")
107
+ width = int.from_bytes(raw[18:22], "little", signed=True)
108
+ height = int.from_bytes(raw[22:26], "little", signed=True)
109
+ bits_per_pixel = int.from_bytes(raw[28:30], "little")
110
+ compression = int.from_bytes(raw[30:34], "little")
111
+
112
+ return {
113
+ "width": width,
114
+ "height": abs(height),
115
+ "top_down": height < 0,
116
+ "bits_per_pixel": bits_per_pixel,
117
+ "compression": compression,
118
+ "pixel_data_offset": pixel_data_offset,
119
+ "byte_size": file_size,
120
+ }
121
+
122
+
123
+ def read_wav(path: str) -> dict:
124
+ with wave.open(path, "rb") as audio:
125
+ frame_count = audio.getnframes()
126
+ sample_rate = audio.getframerate()
127
+ duration = frame_count / sample_rate if sample_rate else 0.0
128
+
129
+ return {
130
+ "channels": audio.getnchannels(),
131
+ "sample_width_bytes": audio.getsampwidth(),
132
+ "sample_rate_hz": sample_rate,
133
+ "frame_count": frame_count,
134
+ "duration_seconds": round(duration, 3),
135
+ "compression_type": audio.getcomptype(),
136
+ }
137
+
138
+
139
+ def read_mp3(path: str) -> dict:
140
+ mpeg_version_names = {0b00: "MPEG 2.5", 0b10: "MPEG 2", 0b11: "MPEG 1"}
141
+ layer_names = {0b01: "Layer III", 0b10: "Layer II", 0b11: "Layer I"}
142
+ channel_mode_names = {0b00: "stereo", 0b01: "joint_stereo", 0b10: "dual_channel", 0b11: "mono"}
143
+ bitrate_table_v1_l3 = [None, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, None]
144
+ sample_rate_table_v1 = [44100, 48000, 32000, None]
145
+
146
+ raw = Path(path).read_bytes()
147
+
148
+ id3_version = None
149
+ id3_size = 0
150
+ if raw[:3] == b"ID3":
151
+ id3_version = f"2.{raw[3]}.{raw[4]}"
152
+ size_bytes = raw[6:10]
153
+ id3_size = (
154
+ ((size_bytes[0] & 0x7F) << 21)
155
+ | ((size_bytes[1] & 0x7F) << 14)
156
+ | ((size_bytes[2] & 0x7F) << 7)
157
+ | (size_bytes[3] & 0x7F)
158
+ ) + 10
159
+
160
+ frame_info = None
161
+ position = id3_size
162
+ while position < len(raw) - 4:
163
+ if raw[position] == 0xFF and (raw[position + 1] & 0xE0) == 0xE0:
164
+ b1, b2, b3, b4 = raw[position:position + 4]
165
+ version_bits = (b2 >> 3) & 0b11
166
+ layer_bits = (b2 >> 1) & 0b11
167
+ bitrate_index = (b3 >> 4) & 0b1111
168
+ sample_rate_index = (b3 >> 2) & 0b11
169
+ channel_mode_bits = (b4 >> 6) & 0b11
170
+
171
+ bitrate_kbps = None
172
+ if version_bits == 0b11 and layer_bits == 0b01 and 0 < bitrate_index < 15:
173
+ bitrate_kbps = bitrate_table_v1_l3[bitrate_index]
174
+
175
+ sample_rate_hz = None
176
+ if version_bits == 0b11 and sample_rate_index < 3:
177
+ sample_rate_hz = sample_rate_table_v1[sample_rate_index]
178
+
179
+ frame_info = {
180
+ "mpeg_version": mpeg_version_names.get(version_bits, "unknown"),
181
+ "layer": layer_names.get(layer_bits, "unknown"),
182
+ "bitrate_kbps": bitrate_kbps,
183
+ "sample_rate_hz": sample_rate_hz,
184
+ "channel_mode": channel_mode_names.get(channel_mode_bits, "unknown"),
185
+ "offset": position,
186
+ }
187
+ break
188
+
189
+ position += 1
190
+
191
+ return {
192
+ "id3_version": id3_version,
193
+ "id3_tag_size": id3_size,
194
+ "first_frame": frame_info,
195
+ "byte_size": len(raw),
196
+ }
197
+
198
+
199
+ def read_zip(path: str) -> dict:
200
+ with zipfile.ZipFile(path) as archive:
201
+ first_corrupt_file = archive.testzip()
202
+
203
+ entries = []
204
+ for info in archive.infolist():
205
+ entries.append({
206
+ "name": info.filename,
207
+ "is_folder": info.is_dir(),
208
+ "uncompressed_size": info.file_size,
209
+ "compressed_size": info.compress_size,
210
+ "last_modified": "%04d-%02d-%02d %02d:%02d:%02d" % info.date_time,
211
+ "checksum_crc32": format(info.CRC, "08x"),
212
+ })
213
+
214
+ return {
215
+ "first_corrupt_file": first_corrupt_file,
216
+ "entries": entries,
217
+ }
218
+
219
+
220
+ def read_tar(path: str) -> dict:
221
+ with tarfile.open(path) as archive:
222
+ entries = []
223
+ for member in archive.getmembers():
224
+ entries.append({
225
+ "name": member.name,
226
+ "is_folder": member.isdir(),
227
+ "size": member.size,
228
+ "mode": oct(member.mode),
229
+ "modified_unix": member.mtime,
230
+ })
231
+
232
+ return {
233
+ "entries": entries,
234
+ }
235
+
236
+
237
+ def read_gzip(path: str) -> dict:
238
+ FLAG_EXTRA_FIELD = 0b00000100
239
+ FLAG_HAS_FILENAME = 0b00001000
240
+ FLAG_HAS_COMMENT = 0b00010000
241
+
242
+ raw = Path(path).read_bytes()
243
+ flags = raw[3]
244
+ position = 10
245
+
246
+ if flags & FLAG_EXTRA_FIELD:
247
+ extra_length = int.from_bytes(raw[position:position + 2], "little")
248
+ position += 2 + extra_length
249
+
250
+ original_name = None
251
+ if flags & FLAG_HAS_FILENAME:
252
+ end = raw.index(b"\x00", position)
253
+ original_name = raw[position:end].decode("latin-1")
254
+ position = end + 1
255
+
256
+ comment = None
257
+ if flags & FLAG_HAS_COMMENT:
258
+ end = raw.index(b"\x00", position)
259
+ comment = raw[position:end].decode("latin-1")
260
+ position = end + 1
261
+
262
+ return {
263
+ "modified_time_unix": int.from_bytes(raw[4:8], "little"),
264
+ "original_filename": original_name,
265
+ "comment": comment,
266
+ "decompressed_size_bytes": len(gzip.decompress(raw)),
267
+ }
268
+
269
+
270
+ def read_pdf(path: str) -> dict:
271
+ raw = Path(path).read_bytes()
272
+
273
+ version_match = re.match(rb"%PDF-(\d+\.\d+)", raw)
274
+ declared_size_match = re.search(rb"/Size\s+(\d+)", raw)
275
+
276
+ object_ids_found = set(re.findall(rb"(\d+)\s+(\d+)\s+obj\b", raw))
277
+ page_count = len(re.findall(rb"/Type\s*/Page\b", raw))
278
+
279
+ return {
280
+ "pdf_version": version_match.group(1).decode() if version_match else None,
281
+ "declared_object_count": int(declared_size_match.group(1)) if declared_size_match else None,
282
+ "objects_actually_found": len(object_ids_found),
283
+ "page_count": page_count,
284
+ "is_encrypted": b"/Encrypt" in raw,
285
+ "ends_with_eof_marker": raw.rstrip().endswith(b"%%EOF"),
286
+ "byte_size": len(raw),
287
+ }
288
+
289
+
290
+ def read_pe(path: str) -> dict:
291
+ DLL_CHARACTERISTIC_FLAG = 0x2000
292
+
293
+ machine_type_names = {
294
+ 0x014C: "x86 (32-bit)",
295
+ 0x8664: "x86-64 (64-bit)",
296
+ 0xAA64: "ARM64",
297
+ }
298
+ subsystem_names = {
299
+ 2: "windows_gui",
300
+ 3: "windows_console",
301
+ }
302
+
303
+ raw = Path(path).read_bytes()
304
+
305
+ pe_header_offset = int.from_bytes(raw[0x3C:0x40], "little")
306
+
307
+ coff_header = raw[pe_header_offset + 4: pe_header_offset + 24]
308
+ machine = int.from_bytes(coff_header[0:2], "little")
309
+ section_count = int.from_bytes(coff_header[2:4], "little")
310
+ timestamp = int.from_bytes(coff_header[4:8], "little")
311
+ characteristics = int.from_bytes(coff_header[18:20], "little")
312
+ optional_header_size = int.from_bytes(coff_header[16:18], "little")
313
+
314
+ optional_header = raw[pe_header_offset + 24: pe_header_offset + 24 + optional_header_size]
315
+ subsystem = None
316
+ if optional_header_size >= 70:
317
+ subsystem = int.from_bytes(optional_header[68:70], "little")
318
+
319
+ is_dll = bool(characteristics & DLL_CHARACTERISTIC_FLAG)
320
+
321
+ return {
322
+ "architecture": machine_type_names.get(machine, f"unknown (0x{machine:04x})"),
323
+ "file_kind": "dll" if is_dll else "exe",
324
+ "section_count": section_count,
325
+ "build_timestamp_unix": timestamp,
326
+ "subsystem": subsystem_names.get(subsystem, subsystem),
327
+ "byte_size": len(raw),
328
+ }
329
+
330
+
331
+ def read_elf(path: str) -> dict:
332
+ type_names = {1: "relocatable", 2: "executable", 3: "shared_object", 4: "core"}
333
+ machine_names = {0x03: "x86", 0x3E: "x86-64", 0x28: "ARM", 0xB7: "ARM64"}
334
+
335
+ raw = Path(path).read_bytes()
336
+ if raw[:4] != b"\x7fELF":
337
+ return {"error": "not an ELF file"}
338
+
339
+ is_64bit = raw[4] == 2
340
+ endianness = "little" if raw[5] == 1 else "big"
341
+ e_type = int.from_bytes(raw[16:18], endianness)
342
+ e_machine = int.from_bytes(raw[18:20], endianness)
343
+
344
+ return {
345
+ "class": "ELF64" if is_64bit else "ELF32",
346
+ "endianness": endianness,
347
+ "type": type_names.get(e_type, f"unknown ({e_type})"),
348
+ "machine": machine_names.get(e_machine, f"unknown (0x{e_machine:02x})"),
349
+ "byte_size": len(raw),
350
+ }
351
+
352
+
353
+ def _walk_mp4_boxes(data: bytes, start: int, end: int):
354
+ position = start
355
+ while position + 8 <= end:
356
+ box_size = int.from_bytes(data[position:position + 4], "big")
357
+ box_type = data[position + 4:position + 8].decode("latin-1")
358
+ content_start = position + 8
359
+
360
+ if box_size == 1:
361
+ box_size = int.from_bytes(data[position + 8:position + 16], "big")
362
+ content_start = position + 16
363
+ elif box_size == 0:
364
+ box_size = end - position
365
+
366
+ box_end = position + box_size
367
+ if box_end <= content_start or box_end > end:
368
+ break
369
+
370
+ yield box_type, position, content_start, box_end
371
+ position = box_end
372
+
373
+
374
+ def _read_movie_header(data: bytes, content_start: int) -> dict | None:
375
+ version = data[content_start]
376
+
377
+ if version == 1:
378
+ timescale = int.from_bytes(data[content_start + 20:content_start + 24], "big")
379
+ duration_units = int.from_bytes(data[content_start + 24:content_start + 32], "big")
380
+ else:
381
+ timescale = int.from_bytes(data[content_start + 12:content_start + 16], "big")
382
+ duration_units = int.from_bytes(data[content_start + 16:content_start + 20], "big")
383
+
384
+ if not timescale:
385
+ return None
386
+
387
+ return {
388
+ "timescale": timescale,
389
+ "duration_units": duration_units,
390
+ "duration_seconds": round(duration_units / timescale, 3),
391
+ }
392
+
393
+
394
+ def read_mp4(path: str) -> dict:
395
+ raw = Path(path).read_bytes()
396
+
397
+ major_brand = None
398
+ compatible_brands = []
399
+ movie_info = None
400
+ box_list = []
401
+
402
+ for box_type, box_start, content_start, box_end in _walk_mp4_boxes(raw, 0, len(raw)):
403
+ box_list.append({
404
+ "type": box_type,
405
+ "offset": box_start,
406
+ "size": box_end - box_start,
407
+ })
408
+
409
+ if box_type == "ftyp":
410
+ major_brand = raw[content_start:content_start + 4].decode("latin-1").strip()
411
+ compatible_brands = [
412
+ raw[i:i + 4].decode("latin-1").strip()
413
+ for i in range(content_start + 8, box_end, 4)
414
+ ]
415
+
416
+ elif box_type == "moov":
417
+ for inner_type, _s, inner_content_start, _e in _walk_mp4_boxes(raw, content_start, box_end):
418
+ if inner_type == "mvhd":
419
+ movie_info = _read_movie_header(raw, inner_content_start)
420
+ break
421
+
422
+ return {
423
+ "major_brand": major_brand,
424
+ "compatible_brands": compatible_brands,
425
+ "box_count": len(box_list),
426
+ "boxes": box_list,
427
+ "movie_info": movie_info,
428
+ "byte_size": len(raw),
429
+ }
430
+
431
+
432
+ READERS = {
433
+ "png": read_png,
434
+ "jpeg": read_jpeg,
435
+ "jpg": read_jpeg,
436
+ "bmp": read_bmp,
437
+ "wav": read_wav,
438
+ "mp3": read_mp3,
439
+ "zip": read_zip,
440
+ "tar": read_tar,
441
+ "gzip": read_gzip,
442
+ "pdf": read_pdf,
443
+ "exe": read_pe,
444
+ "dll": read_pe,
445
+ "elf": read_elf,
446
+ "mp4": read_mp4,
447
+ }
448
+
449
+
450
+ if __name__ == "__main__":
451
+ print(read_mp3('file.mp3'))