deepbom 0.1.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.
- deepbom-0.1.0/LICENSE +15 -0
- deepbom-0.1.0/PKG-INFO +139 -0
- deepbom-0.1.0/README.md +98 -0
- deepbom-0.1.0/pyproject.toml +41 -0
- deepbom-0.1.0/setup.cfg +4 -0
- deepbom-0.1.0/src/deepbom/__init__.py +12 -0
- deepbom-0.1.0/src/deepbom/__main__.py +98 -0
- deepbom-0.1.0/src/deepbom/inspect.py +310 -0
- deepbom-0.1.0/src/deepbom.egg-info/PKG-INFO +139 -0
- deepbom-0.1.0/src/deepbom.egg-info/SOURCES.txt +11 -0
- deepbom-0.1.0/src/deepbom.egg-info/dependency_links.txt +1 -0
- deepbom-0.1.0/src/deepbom.egg-info/entry_points.txt +2 -0
- deepbom-0.1.0/src/deepbom.egg-info/top_level.txt +1 -0
deepbom-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Copyright (C) 2026 Jun-Hwan Kwon. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This repository and its generated software artifacts are currently provided as
|
|
4
|
+
private research software. No license is granted to copy, modify, distribute,
|
|
5
|
+
sublicense, reverse engineer, or create derivative works from the source code,
|
|
6
|
+
WebAssembly modules, generated JavaScript bindings, or packaged executables,
|
|
7
|
+
except where a separate file or component carries an explicit license.
|
|
8
|
+
|
|
9
|
+
Access to the hosted service does not grant a software or implementation
|
|
10
|
+
license.
|
|
11
|
+
|
|
12
|
+
Future public releases may license selected contracts, conformance fixtures,
|
|
13
|
+
validation data, or automation clients separately. A license applies only to
|
|
14
|
+
the files and versions that explicitly carry it. Third-party dependencies and
|
|
15
|
+
model artifacts remain subject to their respective licenses.
|
deepbom-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: deepbom
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Deployment-artifact inspection for on-device neural network models
|
|
5
|
+
Author: Jun-Hwan Kwon
|
|
6
|
+
License: Copyright (C) 2026 Jun-Hwan Kwon. All rights reserved.
|
|
7
|
+
|
|
8
|
+
This repository and its generated software artifacts are currently provided as
|
|
9
|
+
private research software. No license is granted to copy, modify, distribute,
|
|
10
|
+
sublicense, reverse engineer, or create derivative works from the source code,
|
|
11
|
+
WebAssembly modules, generated JavaScript bindings, or packaged executables,
|
|
12
|
+
except where a separate file or component carries an explicit license.
|
|
13
|
+
|
|
14
|
+
Access to the hosted service does not grant a software or implementation
|
|
15
|
+
license.
|
|
16
|
+
|
|
17
|
+
Future public releases may license selected contracts, conformance fixtures,
|
|
18
|
+
validation data, or automation clients separately. A license applies only to
|
|
19
|
+
the files and versions that explicitly carry it. Third-party dependencies and
|
|
20
|
+
model artifacts remain subject to their respective licenses.
|
|
21
|
+
|
|
22
|
+
Project-URL: Homepage, https://deepbom.org
|
|
23
|
+
Keywords: gguf,safetensors,tflite,onnx,coreml,on-device,edge-ai,quantization,ml-bom,static-analysis
|
|
24
|
+
Classifier: Development Status :: 3 - Alpha
|
|
25
|
+
Classifier: Intended Audience :: Developers
|
|
26
|
+
Classifier: Intended Audience :: Science/Research
|
|
27
|
+
Classifier: License :: OSI Approved :: ISC License (ISCL)
|
|
28
|
+
Classifier: Programming Language :: Python :: 3
|
|
29
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
30
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
31
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
32
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
33
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
34
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
35
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
36
|
+
Classifier: Typing :: Typed
|
|
37
|
+
Requires-Python: >=3.9
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
License-File: LICENSE
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
# deepbom
|
|
43
|
+
|
|
44
|
+
Deployment-artifact inspection for on-device neural network models.
|
|
45
|
+
|
|
46
|
+
Identifies model artifact formats from their container signature and reads the
|
|
47
|
+
contracts that can be decoded without loading tensor payload values.
|
|
48
|
+
|
|
49
|
+
```console
|
|
50
|
+
$ pip install deepbom
|
|
51
|
+
$ deepbom inspect model.safetensors
|
|
52
|
+
|
|
53
|
+
model.safetensors
|
|
54
|
+
sha256 a35fd03f52c12f4e78a246bec1927e9a169377fbb8905dc13165d285010e7a44
|
|
55
|
+
format safetensors size 2.6 MB
|
|
56
|
+
evidence u64 header length followed by a JSON header
|
|
57
|
+
|
|
58
|
+
tensors
|
|
59
|
+
count 38
|
|
60
|
+
parameters 1,377,408
|
|
61
|
+
stored payload 2.6 MB
|
|
62
|
+
dtypes F16 x38
|
|
63
|
+
|
|
64
|
+
Container-level facts only. No tensor payload values were read.
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```console
|
|
68
|
+
$ deepbom inspect model.gguf
|
|
69
|
+
|
|
70
|
+
model.gguf
|
|
71
|
+
sha256 cb95a6e10f28b76a1dd71c15560dec5a5eee8943f591ef45d11c129786b22cff
|
|
72
|
+
format gguf size 509.0 KB
|
|
73
|
+
evidence magic "GGUF" at offset 0
|
|
74
|
+
|
|
75
|
+
container
|
|
76
|
+
gguf version 3
|
|
77
|
+
tensors 39
|
|
78
|
+
metadata 26 / 26 (complete)
|
|
79
|
+
architecture llama
|
|
80
|
+
file type 2
|
|
81
|
+
quant version 2
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## What it reads
|
|
85
|
+
|
|
86
|
+
| Format | Reported |
|
|
87
|
+
| --- | --- |
|
|
88
|
+
| **SafeTensors** | tensor inventory, dtypes, shapes, parameter count, stored payload bytes, metadata, header/file size agreement |
|
|
89
|
+
| **GGUF** | version, tensor count, full metadata key/value inventory, architecture, file type, quantization version |
|
|
90
|
+
| TFLite, ONNX, Core ML | format identification and SHA-256 only |
|
|
91
|
+
|
|
92
|
+
Format is decided from container evidence — FlatBuffer identifier, magic
|
|
93
|
+
bytes, header structure — never from the filename extension. ONNX and Core ML
|
|
94
|
+
are separated by their protobuf field layout rather than guessed.
|
|
95
|
+
|
|
96
|
+
## Usage
|
|
97
|
+
|
|
98
|
+
```console
|
|
99
|
+
deepbom inspect <file>
|
|
100
|
+
deepbom inspect <file> --json
|
|
101
|
+
deepbom --version
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
As a library:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from deepbom import inspect
|
|
108
|
+
|
|
109
|
+
artifact = inspect("model.gguf")
|
|
110
|
+
print(artifact.format, artifact.sha256)
|
|
111
|
+
print(artifact.detail["architecture"])
|
|
112
|
+
print(artifact.to_dict())
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Scope
|
|
116
|
+
|
|
117
|
+
This package is pure Python with no dependencies. It reports **container-level
|
|
118
|
+
facts only**: what the header and directory structures determine. Tensor
|
|
119
|
+
payload values are never read, and nothing is inferred that the container does
|
|
120
|
+
not state.
|
|
121
|
+
|
|
122
|
+
Graph structure, quantization contracts, predicted delegate placement,
|
|
123
|
+
target-profile cost projections and CycloneDX ML-BOM export are **not** part of
|
|
124
|
+
this package. For TFLite graph analysis:
|
|
125
|
+
|
|
126
|
+
```console
|
|
127
|
+
npx deepbom audit model.tflite
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
ONNX, Core ML and runtime evidence are available in the browser version at
|
|
131
|
+
<https://deepbom.org>.
|
|
132
|
+
|
|
133
|
+
## Privacy
|
|
134
|
+
|
|
135
|
+
No network access. Model bytes, filenames and results are never uploaded.
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
ISC — see [LICENSE](./LICENSE).
|
deepbom-0.1.0/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# deepbom
|
|
2
|
+
|
|
3
|
+
Deployment-artifact inspection for on-device neural network models.
|
|
4
|
+
|
|
5
|
+
Identifies model artifact formats from their container signature and reads the
|
|
6
|
+
contracts that can be decoded without loading tensor payload values.
|
|
7
|
+
|
|
8
|
+
```console
|
|
9
|
+
$ pip install deepbom
|
|
10
|
+
$ deepbom inspect model.safetensors
|
|
11
|
+
|
|
12
|
+
model.safetensors
|
|
13
|
+
sha256 a35fd03f52c12f4e78a246bec1927e9a169377fbb8905dc13165d285010e7a44
|
|
14
|
+
format safetensors size 2.6 MB
|
|
15
|
+
evidence u64 header length followed by a JSON header
|
|
16
|
+
|
|
17
|
+
tensors
|
|
18
|
+
count 38
|
|
19
|
+
parameters 1,377,408
|
|
20
|
+
stored payload 2.6 MB
|
|
21
|
+
dtypes F16 x38
|
|
22
|
+
|
|
23
|
+
Container-level facts only. No tensor payload values were read.
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```console
|
|
27
|
+
$ deepbom inspect model.gguf
|
|
28
|
+
|
|
29
|
+
model.gguf
|
|
30
|
+
sha256 cb95a6e10f28b76a1dd71c15560dec5a5eee8943f591ef45d11c129786b22cff
|
|
31
|
+
format gguf size 509.0 KB
|
|
32
|
+
evidence magic "GGUF" at offset 0
|
|
33
|
+
|
|
34
|
+
container
|
|
35
|
+
gguf version 3
|
|
36
|
+
tensors 39
|
|
37
|
+
metadata 26 / 26 (complete)
|
|
38
|
+
architecture llama
|
|
39
|
+
file type 2
|
|
40
|
+
quant version 2
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## What it reads
|
|
44
|
+
|
|
45
|
+
| Format | Reported |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| **SafeTensors** | tensor inventory, dtypes, shapes, parameter count, stored payload bytes, metadata, header/file size agreement |
|
|
48
|
+
| **GGUF** | version, tensor count, full metadata key/value inventory, architecture, file type, quantization version |
|
|
49
|
+
| TFLite, ONNX, Core ML | format identification and SHA-256 only |
|
|
50
|
+
|
|
51
|
+
Format is decided from container evidence — FlatBuffer identifier, magic
|
|
52
|
+
bytes, header structure — never from the filename extension. ONNX and Core ML
|
|
53
|
+
are separated by their protobuf field layout rather than guessed.
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
```console
|
|
58
|
+
deepbom inspect <file>
|
|
59
|
+
deepbom inspect <file> --json
|
|
60
|
+
deepbom --version
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
As a library:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from deepbom import inspect
|
|
67
|
+
|
|
68
|
+
artifact = inspect("model.gguf")
|
|
69
|
+
print(artifact.format, artifact.sha256)
|
|
70
|
+
print(artifact.detail["architecture"])
|
|
71
|
+
print(artifact.to_dict())
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Scope
|
|
75
|
+
|
|
76
|
+
This package is pure Python with no dependencies. It reports **container-level
|
|
77
|
+
facts only**: what the header and directory structures determine. Tensor
|
|
78
|
+
payload values are never read, and nothing is inferred that the container does
|
|
79
|
+
not state.
|
|
80
|
+
|
|
81
|
+
Graph structure, quantization contracts, predicted delegate placement,
|
|
82
|
+
target-profile cost projections and CycloneDX ML-BOM export are **not** part of
|
|
83
|
+
this package. For TFLite graph analysis:
|
|
84
|
+
|
|
85
|
+
```console
|
|
86
|
+
npx deepbom audit model.tflite
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
ONNX, Core ML and runtime evidence are available in the browser version at
|
|
90
|
+
<https://deepbom.org>.
|
|
91
|
+
|
|
92
|
+
## Privacy
|
|
93
|
+
|
|
94
|
+
No network access. Model bytes, filenames and results are never uploaded.
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
ISC — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "deepbom"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Deployment-artifact inspection for on-device neural network models"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
authors = [{ name = "Jun-Hwan Kwon" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"gguf", "safetensors", "tflite", "onnx", "coreml",
|
|
15
|
+
"on-device", "edge-ai", "quantization", "ml-bom", "static-analysis",
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Intended Audience :: Science/Research",
|
|
21
|
+
"License :: OSI Approved :: ISC License (ISCL)",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.9",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
29
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
30
|
+
"Typing :: Typed",
|
|
31
|
+
]
|
|
32
|
+
dependencies = []
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://deepbom.org"
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
deepbom = "deepbom.__main__:main"
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.packages.find]
|
|
41
|
+
where = ["src"]
|
deepbom-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Deployment-artifact inspection for on-device neural network models.
|
|
2
|
+
|
|
3
|
+
Identifies model artifact formats and reads the container-level contracts that
|
|
4
|
+
can be decoded without loading tensor payload values.
|
|
5
|
+
|
|
6
|
+
This package performs no network access. Model bytes stay on the machine.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .inspect import Artifact, InspectionError, identify, inspect
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.0"
|
|
12
|
+
__all__ = ["Artifact", "InspectionError", "identify", "inspect", "__version__"]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Command line entry point: ``deepbom`` / ``python -m deepbom``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from . import __version__
|
|
10
|
+
from .inspect import Artifact, InspectionError, inspect
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _fmt_bytes(value: int) -> str:
|
|
14
|
+
size = float(value)
|
|
15
|
+
for unit in ("B", "KB", "MB", "GB"):
|
|
16
|
+
if size < 1024 or unit == "GB":
|
|
17
|
+
return f"{size:.0f} {unit}" if unit == "B" else f"{size:.1f} {unit}"
|
|
18
|
+
size /= 1024
|
|
19
|
+
return f"{size:.1f} GB"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _render(artifact: Artifact) -> str:
|
|
23
|
+
out: list[str] = []
|
|
24
|
+
detail = artifact.detail
|
|
25
|
+
out.append(artifact.path)
|
|
26
|
+
out.append(f" sha256 {artifact.sha256}")
|
|
27
|
+
out.append(f" format {artifact.format} size {_fmt_bytes(artifact.size_bytes)}")
|
|
28
|
+
out.append(f" evidence {artifact.format_evidence}")
|
|
29
|
+
out.append("")
|
|
30
|
+
|
|
31
|
+
if artifact.format == "safetensors":
|
|
32
|
+
out.append("tensors")
|
|
33
|
+
out.append(f" count {detail['tensor_count']:,}")
|
|
34
|
+
out.append(f" parameters {detail['parameter_count']:,}")
|
|
35
|
+
out.append(f" stored payload {_fmt_bytes(detail['stored_payload_bytes'])}")
|
|
36
|
+
dtypes = ", ".join(f"{k} x{v}" for k, v in detail["dtype_counts"].items())
|
|
37
|
+
out.append(f" dtypes {dtypes}")
|
|
38
|
+
if detail.get("metadata"):
|
|
39
|
+
out.append(f" metadata keys {', '.join(sorted(detail['metadata'])[:6])}")
|
|
40
|
+
out.append("")
|
|
41
|
+
elif artifact.format == "gguf":
|
|
42
|
+
out.append("container")
|
|
43
|
+
out.append(f" gguf version {detail['gguf_version']}")
|
|
44
|
+
out.append(f" tensors {detail['tensor_count']:,}")
|
|
45
|
+
complete = "complete" if detail.get("metadata_complete") else "partial"
|
|
46
|
+
out.append(
|
|
47
|
+
f" metadata {detail['metadata_decoded_count']:,} / "
|
|
48
|
+
f"{detail['metadata_kv_count']:,} ({complete})"
|
|
49
|
+
)
|
|
50
|
+
for label, key in (("architecture", "architecture"), ("name", "name"),
|
|
51
|
+
("file type", "file_type"), ("quant version", "quantization_version")):
|
|
52
|
+
if detail.get(key) is not None:
|
|
53
|
+
out.append(f" {label:<18} {detail[key]}")
|
|
54
|
+
out.append("")
|
|
55
|
+
|
|
56
|
+
if artifact.notes:
|
|
57
|
+
out.append("notes")
|
|
58
|
+
for note in artifact.notes:
|
|
59
|
+
out.append(f" - {note}")
|
|
60
|
+
out.append("")
|
|
61
|
+
|
|
62
|
+
out.append("Container-level facts only. No tensor payload values were read.")
|
|
63
|
+
return "\n".join(out)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def main(argv: list[str] | None = None) -> int:
|
|
67
|
+
parser = argparse.ArgumentParser(
|
|
68
|
+
prog="deepbom",
|
|
69
|
+
description="Deployment-artifact inspection for on-device neural network models.",
|
|
70
|
+
epilog="Graph, quantization and delegate analysis: `npx deepbom audit` or https://deepbom.org",
|
|
71
|
+
)
|
|
72
|
+
parser.add_argument("--version", action="version", version=__version__)
|
|
73
|
+
sub = parser.add_subparsers(dest="command")
|
|
74
|
+
|
|
75
|
+
inspect_cmd = sub.add_parser("inspect", help="Inspect an artifact container")
|
|
76
|
+
inspect_cmd.add_argument("path", help="Path to a model artifact")
|
|
77
|
+
inspect_cmd.add_argument("--json", action="store_true", help="Emit JSON")
|
|
78
|
+
|
|
79
|
+
args = parser.parse_args(argv)
|
|
80
|
+
if args.command != "inspect":
|
|
81
|
+
parser.print_help()
|
|
82
|
+
return 1
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
artifact = inspect(args.path)
|
|
86
|
+
except InspectionError as error:
|
|
87
|
+
print(f"deepbom: {error}", file=sys.stderr)
|
|
88
|
+
return 2
|
|
89
|
+
|
|
90
|
+
if args.json:
|
|
91
|
+
print(json.dumps(artifact.to_dict(), indent=2, ensure_ascii=False))
|
|
92
|
+
else:
|
|
93
|
+
print(_render(artifact))
|
|
94
|
+
return 0
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""Format identification and container-level inspection.
|
|
2
|
+
|
|
3
|
+
Only facts the serialized container determines are reported. Nothing is
|
|
4
|
+
inferred from a filename extension, and tensor payload values are never read.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import struct
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
# GGUF value type ids, ggml/gguf.md
|
|
17
|
+
_GGUF_TYPES = {
|
|
18
|
+
0: ("uint8", "<B", 1), 1: ("int8", "<b", 1),
|
|
19
|
+
2: ("uint16", "<H", 2), 3: ("int16", "<h", 2),
|
|
20
|
+
4: ("uint32", "<I", 4), 5: ("int32", "<i", 4),
|
|
21
|
+
6: ("float32", "<f", 4), 7: ("bool", "<?", 1),
|
|
22
|
+
8: ("string", None, None), 9: ("array", None, None),
|
|
23
|
+
10: ("uint64", "<Q", 8), 11: ("int64", "<q", 8),
|
|
24
|
+
12: ("float64", "<d", 8),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_SAFETENSORS_DTYPE_BITS = {
|
|
28
|
+
"BOOL": 1, "U8": 8, "I8": 8, "F8_E4M3": 8, "F8_E5M2": 8,
|
|
29
|
+
"U16": 16, "I16": 16, "F16": 16, "BF16": 16,
|
|
30
|
+
"U32": 32, "I32": 32, "F32": 32,
|
|
31
|
+
"U64": 64, "I64": 64, "F64": 64,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_READ_LIMIT = 64 * 1024 * 1024 # header scan ceiling
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class InspectionError(Exception):
|
|
38
|
+
"""Raised when a file cannot be inspected as a supported artifact."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class Artifact:
|
|
43
|
+
"""Container-level facts about one artifact."""
|
|
44
|
+
|
|
45
|
+
path: str
|
|
46
|
+
size_bytes: int
|
|
47
|
+
sha256: str
|
|
48
|
+
format: str
|
|
49
|
+
format_evidence: str
|
|
50
|
+
detail: dict[str, Any] = field(default_factory=dict)
|
|
51
|
+
notes: list[str] = field(default_factory=list)
|
|
52
|
+
|
|
53
|
+
def to_dict(self) -> dict[str, Any]:
|
|
54
|
+
return {
|
|
55
|
+
"schema": "deepbom.artifact_inspection.v1",
|
|
56
|
+
"path": self.path,
|
|
57
|
+
"size_bytes": self.size_bytes,
|
|
58
|
+
"sha256": self.sha256,
|
|
59
|
+
"format": self.format,
|
|
60
|
+
"format_evidence": self.format_evidence,
|
|
61
|
+
"detail": self.detail,
|
|
62
|
+
"notes": self.notes,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _sha256(path: Path) -> str:
|
|
67
|
+
digest = hashlib.sha256()
|
|
68
|
+
with path.open("rb") as handle:
|
|
69
|
+
for block in iter(lambda: handle.read(1 << 20), b""):
|
|
70
|
+
digest.update(block)
|
|
71
|
+
return digest.hexdigest()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def identify(head: bytes) -> tuple[str, str]:
|
|
75
|
+
"""Return (format, evidence) from leading bytes alone."""
|
|
76
|
+
if len(head) >= 8 and head[4:8] == b"TFL3":
|
|
77
|
+
return "tflite", 'FlatBuffer file identifier "TFL3" at offset 4'
|
|
78
|
+
if head[:4] == b"GGUF":
|
|
79
|
+
return "gguf", 'magic "GGUF" at offset 0'
|
|
80
|
+
if head[:4] == b"PK\x03\x04":
|
|
81
|
+
return "zip-container", "ZIP local file header — Core ML .mlpackage or archived artifact"
|
|
82
|
+
if len(head) >= 8:
|
|
83
|
+
# SafeTensors: u64 little-endian header length, then a JSON object
|
|
84
|
+
(header_len,) = struct.unpack("<Q", head[:8])
|
|
85
|
+
if 2 <= header_len <= _READ_LIMIT and head[8:9] == b"{":
|
|
86
|
+
return "safetensors", "u64 header length followed by a JSON header"
|
|
87
|
+
protobuf = _classify_protobuf(head)
|
|
88
|
+
if protobuf is not None:
|
|
89
|
+
return protobuf
|
|
90
|
+
return "unknown", "no recognised container signature"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _read_varint(buf: bytes, index: int) -> tuple[int, int]:
|
|
94
|
+
"""Return (value, next_index) for a protobuf varint."""
|
|
95
|
+
value = 0
|
|
96
|
+
shift = 0
|
|
97
|
+
while index < len(buf):
|
|
98
|
+
byte = buf[index]
|
|
99
|
+
value |= (byte & 0x7F) << shift
|
|
100
|
+
index += 1
|
|
101
|
+
if not byte & 0x80:
|
|
102
|
+
return value, index
|
|
103
|
+
shift += 7
|
|
104
|
+
if shift > 63:
|
|
105
|
+
break
|
|
106
|
+
raise InspectionError("truncated varint")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _classify_protobuf(head: bytes) -> tuple[str, str] | None:
|
|
110
|
+
"""Separate ONNX ModelProto from Core ML Model by their second field.
|
|
111
|
+
|
|
112
|
+
Both start with field 1 as a varint. ONNX field 2 is `producer_name`, a
|
|
113
|
+
short printable string; Core ML field 2 is the `description` message.
|
|
114
|
+
"""
|
|
115
|
+
if not head[:1] == b"\x08":
|
|
116
|
+
return None
|
|
117
|
+
try:
|
|
118
|
+
_, index = _read_varint(head, 1)
|
|
119
|
+
if index >= len(head) or head[index] != 0x12:
|
|
120
|
+
return None
|
|
121
|
+
length, after = _read_varint(head, index + 1)
|
|
122
|
+
length_bytes = after - (index + 1)
|
|
123
|
+
payload = head[after:after + min(length, 48)]
|
|
124
|
+
except InspectionError:
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
printable = bool(payload) and all(32 <= byte < 127 for byte in payload)
|
|
128
|
+
if length_bytes == 1 and length <= 64 and printable:
|
|
129
|
+
return "onnx", "protobuf ir_version followed by a printable producer_name string"
|
|
130
|
+
return "coreml", "protobuf specificationVersion followed by a description message"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _inspect_safetensors(path: Path, size: int) -> tuple[dict[str, Any], list[str]]:
|
|
134
|
+
with path.open("rb") as handle:
|
|
135
|
+
(header_len,) = struct.unpack("<Q", handle.read(8))
|
|
136
|
+
if header_len > _READ_LIMIT or 8 + header_len > size:
|
|
137
|
+
raise InspectionError("SafeTensors header length exceeds the file")
|
|
138
|
+
header = json.loads(handle.read(header_len).decode("utf-8"))
|
|
139
|
+
|
|
140
|
+
tensors: list[dict[str, Any]] = []
|
|
141
|
+
dtypes: dict[str, int] = {}
|
|
142
|
+
total_elements = 0
|
|
143
|
+
stored_bytes = 0
|
|
144
|
+
for name, entry in header.items():
|
|
145
|
+
if name == "__metadata__":
|
|
146
|
+
continue
|
|
147
|
+
shape = entry.get("shape", [])
|
|
148
|
+
dtype = entry.get("dtype", "UNKNOWN")
|
|
149
|
+
offsets = entry.get("data_offsets", [0, 0])
|
|
150
|
+
elements = 1
|
|
151
|
+
for dim in shape:
|
|
152
|
+
elements *= int(dim)
|
|
153
|
+
if not shape:
|
|
154
|
+
elements = 1
|
|
155
|
+
total_elements += elements
|
|
156
|
+
stored_bytes += int(offsets[1]) - int(offsets[0])
|
|
157
|
+
dtypes[dtype] = dtypes.get(dtype, 0) + 1
|
|
158
|
+
tensors.append({"name": name, "dtype": dtype, "shape": shape, "elements": elements})
|
|
159
|
+
|
|
160
|
+
detail: dict[str, Any] = {
|
|
161
|
+
"tensor_count": len(tensors),
|
|
162
|
+
"parameter_count": total_elements,
|
|
163
|
+
"stored_payload_bytes": stored_bytes,
|
|
164
|
+
"dtype_counts": dict(sorted(dtypes.items())),
|
|
165
|
+
"metadata": header.get("__metadata__", {}),
|
|
166
|
+
"tensors": tensors,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
notes: list[str] = []
|
|
170
|
+
declared_end = 8 + header_len + stored_bytes
|
|
171
|
+
if declared_end != size:
|
|
172
|
+
notes.append(
|
|
173
|
+
f"declared payload ends at {declared_end} but the file is {size} bytes "
|
|
174
|
+
f"({size - declared_end:+d})"
|
|
175
|
+
)
|
|
176
|
+
for dtype, count in dtypes.items():
|
|
177
|
+
if dtype not in _SAFETENSORS_DTYPE_BITS:
|
|
178
|
+
notes.append(f"unrecognised dtype {dtype!r} on {count} tensor(s)")
|
|
179
|
+
return detail, notes
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _read_gguf_string(handle) -> str:
|
|
183
|
+
(length,) = struct.unpack("<Q", handle.read(8))
|
|
184
|
+
if length > 1 << 20:
|
|
185
|
+
raise InspectionError("GGUF string length is implausible")
|
|
186
|
+
return handle.read(length).decode("utf-8", errors="replace")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _read_gguf_value(handle, type_id: int, depth: int = 0) -> Any:
|
|
190
|
+
if type_id not in _GGUF_TYPES:
|
|
191
|
+
raise InspectionError(f"unknown GGUF value type {type_id}")
|
|
192
|
+
name, fmt, width = _GGUF_TYPES[type_id]
|
|
193
|
+
if name == "string":
|
|
194
|
+
return _read_gguf_string(handle)
|
|
195
|
+
if name == "array":
|
|
196
|
+
if depth > 1:
|
|
197
|
+
raise InspectionError("nested GGUF arrays are not decoded")
|
|
198
|
+
(elem_type,) = struct.unpack("<I", handle.read(4))
|
|
199
|
+
(count,) = struct.unpack("<Q", handle.read(8))
|
|
200
|
+
if count > 1 << 26:
|
|
201
|
+
raise InspectionError("GGUF array length is implausible")
|
|
202
|
+
if elem_type not in _GGUF_TYPES:
|
|
203
|
+
raise InspectionError(f"unknown GGUF array element type {elem_type}")
|
|
204
|
+
elem_name, _, elem_width = _GGUF_TYPES[elem_type]
|
|
205
|
+
if count <= 64:
|
|
206
|
+
return [_read_gguf_value(handle, elem_type, depth + 1) for _ in range(count)]
|
|
207
|
+
# Large arrays (tokenizer vocabularies) are summarised, but their bytes
|
|
208
|
+
# must still be consumed or every following key/value misaligns.
|
|
209
|
+
if elem_width is not None:
|
|
210
|
+
handle.seek(count * elem_width, 1)
|
|
211
|
+
elif elem_name == "string":
|
|
212
|
+
for _ in range(count):
|
|
213
|
+
(length,) = struct.unpack("<Q", handle.read(8))
|
|
214
|
+
if length > 1 << 20:
|
|
215
|
+
raise InspectionError("GGUF string length is implausible")
|
|
216
|
+
handle.seek(length, 1)
|
|
217
|
+
else:
|
|
218
|
+
raise InspectionError(f"cannot skip a large array of {elem_name}")
|
|
219
|
+
return {"array_type": elem_name, "length": count, "elided": True}
|
|
220
|
+
return struct.unpack(fmt, handle.read(width))[0]
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _inspect_gguf(path: Path, size: int) -> tuple[dict[str, Any], list[str]]:
|
|
224
|
+
notes: list[str] = []
|
|
225
|
+
with path.open("rb") as handle:
|
|
226
|
+
handle.read(4) # magic
|
|
227
|
+
version, tensor_count, kv_count = struct.unpack("<IQQ", handle.read(20))
|
|
228
|
+
if version not in (2, 3):
|
|
229
|
+
notes.append(f"GGUF version {version} is outside the versions this reader decodes (2, 3)")
|
|
230
|
+
metadata: dict[str, Any] = {}
|
|
231
|
+
truncated = False
|
|
232
|
+
if version in (2, 3):
|
|
233
|
+
try:
|
|
234
|
+
for _ in range(kv_count):
|
|
235
|
+
key = _read_gguf_string(handle)
|
|
236
|
+
(type_id,) = struct.unpack("<I", handle.read(4))
|
|
237
|
+
metadata[key] = _read_gguf_value(handle, type_id)
|
|
238
|
+
except (InspectionError, struct.error, UnicodeDecodeError) as error:
|
|
239
|
+
truncated = True
|
|
240
|
+
notes.append(f"metadata decoding stopped early: {error}")
|
|
241
|
+
|
|
242
|
+
detail: dict[str, Any] = {
|
|
243
|
+
"gguf_version": version,
|
|
244
|
+
"tensor_count": tensor_count,
|
|
245
|
+
"metadata_kv_count": kv_count,
|
|
246
|
+
"metadata_decoded_count": len(metadata),
|
|
247
|
+
"architecture": metadata.get("general.architecture"),
|
|
248
|
+
"name": metadata.get("general.name"),
|
|
249
|
+
"file_type": metadata.get("general.file_type"),
|
|
250
|
+
"quantization_version": metadata.get("general.quantization_version"),
|
|
251
|
+
"metadata": metadata,
|
|
252
|
+
}
|
|
253
|
+
if truncated:
|
|
254
|
+
detail["metadata_complete"] = False
|
|
255
|
+
else:
|
|
256
|
+
detail["metadata_complete"] = len(metadata) == kv_count
|
|
257
|
+
return detail, notes
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _inspect_tflite(path: Path, size: int) -> tuple[dict[str, Any], list[str]]:
|
|
261
|
+
return (
|
|
262
|
+
{"file_identifier": "TFL3"},
|
|
263
|
+
[
|
|
264
|
+
"Graph, quantization and delegate analysis for TFLite is not implemented in this "
|
|
265
|
+
"pure-Python package. Use `npx deepbom audit` or https://deepbom.org",
|
|
266
|
+
],
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def inspect(path: str | Path) -> Artifact:
|
|
271
|
+
"""Inspect one artifact and return container-level facts."""
|
|
272
|
+
target = Path(path)
|
|
273
|
+
if not target.is_file():
|
|
274
|
+
raise InspectionError(f"not a file: {target}")
|
|
275
|
+
size = target.stat().st_size
|
|
276
|
+
if size == 0:
|
|
277
|
+
raise InspectionError(f"empty file: {target}")
|
|
278
|
+
|
|
279
|
+
with target.open("rb") as handle:
|
|
280
|
+
head = handle.read(1024)
|
|
281
|
+
fmt, evidence = identify(head)
|
|
282
|
+
|
|
283
|
+
detail: dict[str, Any] = {}
|
|
284
|
+
notes: list[str] = []
|
|
285
|
+
try:
|
|
286
|
+
if fmt == "safetensors":
|
|
287
|
+
detail, notes = _inspect_safetensors(target, size)
|
|
288
|
+
elif fmt == "gguf":
|
|
289
|
+
detail, notes = _inspect_gguf(target, size)
|
|
290
|
+
elif fmt == "tflite":
|
|
291
|
+
detail, notes = _inspect_tflite(target, size)
|
|
292
|
+
elif fmt in ("onnx", "coreml", "zip-container"):
|
|
293
|
+
notes = [
|
|
294
|
+
f"{fmt} container inspection is not implemented in this pure-Python package. "
|
|
295
|
+
"Use https://deepbom.org",
|
|
296
|
+
]
|
|
297
|
+
else:
|
|
298
|
+
notes = ["Unrecognised container. No format-specific facts are reported."]
|
|
299
|
+
except (struct.error, UnicodeDecodeError, json.JSONDecodeError) as error:
|
|
300
|
+
raise InspectionError(f"{fmt} header could not be decoded: {error}") from error
|
|
301
|
+
|
|
302
|
+
return Artifact(
|
|
303
|
+
path=str(target),
|
|
304
|
+
size_bytes=size,
|
|
305
|
+
sha256=_sha256(target),
|
|
306
|
+
format=fmt,
|
|
307
|
+
format_evidence=evidence,
|
|
308
|
+
detail=detail,
|
|
309
|
+
notes=notes,
|
|
310
|
+
)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: deepbom
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Deployment-artifact inspection for on-device neural network models
|
|
5
|
+
Author: Jun-Hwan Kwon
|
|
6
|
+
License: Copyright (C) 2026 Jun-Hwan Kwon. All rights reserved.
|
|
7
|
+
|
|
8
|
+
This repository and its generated software artifacts are currently provided as
|
|
9
|
+
private research software. No license is granted to copy, modify, distribute,
|
|
10
|
+
sublicense, reverse engineer, or create derivative works from the source code,
|
|
11
|
+
WebAssembly modules, generated JavaScript bindings, or packaged executables,
|
|
12
|
+
except where a separate file or component carries an explicit license.
|
|
13
|
+
|
|
14
|
+
Access to the hosted service does not grant a software or implementation
|
|
15
|
+
license.
|
|
16
|
+
|
|
17
|
+
Future public releases may license selected contracts, conformance fixtures,
|
|
18
|
+
validation data, or automation clients separately. A license applies only to
|
|
19
|
+
the files and versions that explicitly carry it. Third-party dependencies and
|
|
20
|
+
model artifacts remain subject to their respective licenses.
|
|
21
|
+
|
|
22
|
+
Project-URL: Homepage, https://deepbom.org
|
|
23
|
+
Keywords: gguf,safetensors,tflite,onnx,coreml,on-device,edge-ai,quantization,ml-bom,static-analysis
|
|
24
|
+
Classifier: Development Status :: 3 - Alpha
|
|
25
|
+
Classifier: Intended Audience :: Developers
|
|
26
|
+
Classifier: Intended Audience :: Science/Research
|
|
27
|
+
Classifier: License :: OSI Approved :: ISC License (ISCL)
|
|
28
|
+
Classifier: Programming Language :: Python :: 3
|
|
29
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
30
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
31
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
32
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
33
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
34
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
35
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
36
|
+
Classifier: Typing :: Typed
|
|
37
|
+
Requires-Python: >=3.9
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
License-File: LICENSE
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
# deepbom
|
|
43
|
+
|
|
44
|
+
Deployment-artifact inspection for on-device neural network models.
|
|
45
|
+
|
|
46
|
+
Identifies model artifact formats from their container signature and reads the
|
|
47
|
+
contracts that can be decoded without loading tensor payload values.
|
|
48
|
+
|
|
49
|
+
```console
|
|
50
|
+
$ pip install deepbom
|
|
51
|
+
$ deepbom inspect model.safetensors
|
|
52
|
+
|
|
53
|
+
model.safetensors
|
|
54
|
+
sha256 a35fd03f52c12f4e78a246bec1927e9a169377fbb8905dc13165d285010e7a44
|
|
55
|
+
format safetensors size 2.6 MB
|
|
56
|
+
evidence u64 header length followed by a JSON header
|
|
57
|
+
|
|
58
|
+
tensors
|
|
59
|
+
count 38
|
|
60
|
+
parameters 1,377,408
|
|
61
|
+
stored payload 2.6 MB
|
|
62
|
+
dtypes F16 x38
|
|
63
|
+
|
|
64
|
+
Container-level facts only. No tensor payload values were read.
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```console
|
|
68
|
+
$ deepbom inspect model.gguf
|
|
69
|
+
|
|
70
|
+
model.gguf
|
|
71
|
+
sha256 cb95a6e10f28b76a1dd71c15560dec5a5eee8943f591ef45d11c129786b22cff
|
|
72
|
+
format gguf size 509.0 KB
|
|
73
|
+
evidence magic "GGUF" at offset 0
|
|
74
|
+
|
|
75
|
+
container
|
|
76
|
+
gguf version 3
|
|
77
|
+
tensors 39
|
|
78
|
+
metadata 26 / 26 (complete)
|
|
79
|
+
architecture llama
|
|
80
|
+
file type 2
|
|
81
|
+
quant version 2
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## What it reads
|
|
85
|
+
|
|
86
|
+
| Format | Reported |
|
|
87
|
+
| --- | --- |
|
|
88
|
+
| **SafeTensors** | tensor inventory, dtypes, shapes, parameter count, stored payload bytes, metadata, header/file size agreement |
|
|
89
|
+
| **GGUF** | version, tensor count, full metadata key/value inventory, architecture, file type, quantization version |
|
|
90
|
+
| TFLite, ONNX, Core ML | format identification and SHA-256 only |
|
|
91
|
+
|
|
92
|
+
Format is decided from container evidence — FlatBuffer identifier, magic
|
|
93
|
+
bytes, header structure — never from the filename extension. ONNX and Core ML
|
|
94
|
+
are separated by their protobuf field layout rather than guessed.
|
|
95
|
+
|
|
96
|
+
## Usage
|
|
97
|
+
|
|
98
|
+
```console
|
|
99
|
+
deepbom inspect <file>
|
|
100
|
+
deepbom inspect <file> --json
|
|
101
|
+
deepbom --version
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
As a library:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from deepbom import inspect
|
|
108
|
+
|
|
109
|
+
artifact = inspect("model.gguf")
|
|
110
|
+
print(artifact.format, artifact.sha256)
|
|
111
|
+
print(artifact.detail["architecture"])
|
|
112
|
+
print(artifact.to_dict())
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Scope
|
|
116
|
+
|
|
117
|
+
This package is pure Python with no dependencies. It reports **container-level
|
|
118
|
+
facts only**: what the header and directory structures determine. Tensor
|
|
119
|
+
payload values are never read, and nothing is inferred that the container does
|
|
120
|
+
not state.
|
|
121
|
+
|
|
122
|
+
Graph structure, quantization contracts, predicted delegate placement,
|
|
123
|
+
target-profile cost projections and CycloneDX ML-BOM export are **not** part of
|
|
124
|
+
this package. For TFLite graph analysis:
|
|
125
|
+
|
|
126
|
+
```console
|
|
127
|
+
npx deepbom audit model.tflite
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
ONNX, Core ML and runtime evidence are available in the browser version at
|
|
131
|
+
<https://deepbom.org>.
|
|
132
|
+
|
|
133
|
+
## Privacy
|
|
134
|
+
|
|
135
|
+
No network access. Model bytes, filenames and results are never uploaded.
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
ISC — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/deepbom/__init__.py
|
|
5
|
+
src/deepbom/__main__.py
|
|
6
|
+
src/deepbom/inspect.py
|
|
7
|
+
src/deepbom.egg-info/PKG-INFO
|
|
8
|
+
src/deepbom.egg-info/SOURCES.txt
|
|
9
|
+
src/deepbom.egg-info/dependency_links.txt
|
|
10
|
+
src/deepbom.egg-info/entry_points.txt
|
|
11
|
+
src/deepbom.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
deepbom
|