oat-py 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.
Files changed (38) hide show
  1. oat_py-1.0.0/PKG-INFO +7 -0
  2. oat_py-1.0.0/README.md +330 -0
  3. oat_py-1.0.0/pyproject.toml +23 -0
  4. oat_py-1.0.0/setup.cfg +4 -0
  5. oat_py-1.0.0/src/oat/__init__.py +4 -0
  6. oat_py-1.0.0/src/oat/__main__.py +40 -0
  7. oat_py-1.0.0/src/oat/analysis/__init__.py +0 -0
  8. oat_py-1.0.0/src/oat/analysis/document.py +36 -0
  9. oat_py-1.0.0/src/oat/analysis/file_type.py +136 -0
  10. oat_py-1.0.0/src/oat/analysis/header_reader.py +114 -0
  11. oat_py-1.0.0/src/oat/analysis/pipeline.py +122 -0
  12. oat_py-1.0.0/src/oat/analysis/policy_verifier.py +396 -0
  13. oat_py-1.0.0/src/oat/cli.py +100 -0
  14. oat_py-1.0.0/src/oat/config/__init__.py +0 -0
  15. oat_py-1.0.0/src/oat/config/loader.py +389 -0
  16. oat_py-1.0.0/src/oat/config/schema.py +113 -0
  17. oat_py-1.0.0/src/oat/matchers/__init__.py +0 -0
  18. oat_py-1.0.0/src/oat/matchers/copyright_matcher.py +111 -0
  19. oat_py-1.0.0/src/oat/matchers/license_matcher.py +472 -0
  20. oat_py-1.0.0/src/oat/matchers/spdx_license_loader.py +67 -0
  21. oat_py-1.0.0/src/oat/reporter/__init__.py +0 -0
  22. oat_py-1.0.0/src/oat/reporter/model.py +33 -0
  23. oat_py-1.0.0/src/oat/reporter/plain_reporter.py +94 -0
  24. oat_py-1.0.0/src/oat/resources/OAT-Default.xml +138 -0
  25. oat_py-1.0.0/src/oat/resources/__init__.py +0 -0
  26. oat_py-1.0.0/src/oat/resources/builtin_licenses.json +532 -0
  27. oat_py-1.0.0/src/oat/resources/licenses-exception.json +1 -0
  28. oat_py-1.0.0/src/oat/resources/licenses.json +9804 -0
  29. oat_py-1.0.0/src/oat/utils/__init__.py +0 -0
  30. oat_py-1.0.0/src/oat/utils/text_util.py +37 -0
  31. oat_py-1.0.0/src/oat/walker/__init__.py +0 -0
  32. oat_py-1.0.0/src/oat/walker/directory_walker.py +160 -0
  33. oat_py-1.0.0/src/oat_py.egg-info/PKG-INFO +7 -0
  34. oat_py-1.0.0/src/oat_py.egg-info/SOURCES.txt +36 -0
  35. oat_py-1.0.0/src/oat_py.egg-info/dependency_links.txt +1 -0
  36. oat_py-1.0.0/src/oat_py.egg-info/entry_points.txt +2 -0
  37. oat_py-1.0.0/src/oat_py.egg-info/requires.txt +2 -0
  38. oat_py-1.0.0/src/oat_py.egg-info/top_level.txt +1 -0
oat_py-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: oat-py
3
+ Version: 1.0.0
4
+ Summary: OSS Audit Tool - Python Edition
5
+ Requires-Python: >=3.9
6
+ Requires-Dist: lxml>=5.0
7
+ Requires-Dist: chardet>=5.0
oat_py-1.0.0/README.md ADDED
@@ -0,0 +1,330 @@
1
+ # OAT Python Edition
2
+
3
+ > OpenHarmony OSS Audit Tool —— Python 重写版,无需 Java/JDK 环境,开箱即用。
4
+
5
+ [![Python](https://img.shields.io/badge/python-3.9%2B-blue.svg)](https://www.python.org/)
6
+ [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](../tools_oat/LICENSE)
7
+
8
+ ---
9
+
10
+ ## ✨ 特性
11
+
12
+ | 特性 | 说明 |
13
+ |------|------|
14
+ | **零 Java 依赖** | 仅需 Python 3.9+,告别 JDK/Maven 安装烦恼 |
15
+ | **高性能** | 并发扫描,速度比 Java 版快约 25 倍(143 个文件 0.6s) |
16
+ | **完全兼容** | 读取项目 `OAT.xml`,规则与原版 Java OAT 对齐 |
17
+ | **增量扫描** | 支持只扫描变更文件,适配 pre-commit 工作流 |
18
+ | **报告对齐** | 输出 `PlainReport_*.txt`,格式与原版完全一致 |
19
+
20
+ ---
21
+
22
+ ## 📦 安装
23
+
24
+ ### 方式一:开发模式安装(推荐)
25
+
26
+ ```bash
27
+ pip install -e /path/to/oat_python
28
+ ```
29
+
30
+ 安装后可直接使用 `oat` 命令。
31
+
32
+ ### 方式二:直接运行(无需安装)
33
+
34
+ ```bash
35
+ cd /path/to/oat_python
36
+ PYTHONPATH=src python -m oat [参数]
37
+ ```
38
+
39
+ ### 依赖
40
+
41
+ | 包 | 版本 | 用途 |
42
+ |----|------|------|
43
+ | `lxml` | >=5.0 | 解析 OAT.xml 配置文件 |
44
+ | `chardet` | >=5.0 | 源文件编码自动检测 |
45
+
46
+ ```bash
47
+ pip install lxml chardet
48
+ ```
49
+
50
+ ---
51
+
52
+ ## 🚀 快速开始
53
+
54
+ ### 全量扫描
55
+
56
+ ```bash
57
+ oat -mode s \
58
+ -s /path/to/your/repo \
59
+ -r /path/to/reports \
60
+ -n YourRepoName
61
+ ```
62
+
63
+ ### 增量扫描(pre-commit 场景)
64
+
65
+ ```bash
66
+ oat -mode s \
67
+ -s /path/to/your/repo \
68
+ -r /path/to/reports \
69
+ -n YourRepoName \
70
+ -w 1 \
71
+ -f "src/foo.cpp,include/bar.h"
72
+ ```
73
+
74
+ ### 查看报告
75
+
76
+ ```bash
77
+ cat /path/to/reports/PlainReport_YourRepoName.txt
78
+ ```
79
+
80
+ ---
81
+
82
+ ## ⚙️ 命令行参数
83
+
84
+ | 参数 | 必填 | 说明 |
85
+ |------|------|------|
86
+ | `-mode s` | 是 | 运行模式,当前支持 `s`(单项目模式) |
87
+ | `-s SOURCE_DIR` | 是 | 被扫描的源码根目录 |
88
+ | `-r REPORT_DIR` | 是 | 报告输出目录(不存在时自动创建) |
89
+ | `-n REPO_NAME` | 是 | 项目名称,用于报告文件命名 |
90
+ | `-w 0\|1` | 否 | 扫描模式:`0`=全量(默认),`1`=增量 |
91
+ | `-f FILE_LIST` | 否 | 增量扫描文件列表,逗号分隔(`-w 1` 时使用) |
92
+ | `-oatconfig PATH` | 否 | 指定自定义 `OAT-Default.xml` 路径(默认使用内置) |
93
+ | `-ignorePrjOAT` | 否 | 忽略项目根目录下的 `OAT.xml` |
94
+ | `-policy STRING` | 否 | 命令行直接覆盖 policy 规则(见下方说明) |
95
+ | `-filter STRING` | 否 | 命令行直接覆盖 filter 规则(见下方说明) |
96
+ | `-v` | 否 | 打印版本号并退出 |
97
+
98
+ ### `-policy` 格式
99
+
100
+ ```
101
+ type:name[@path][~rule][|name2[@path2]] 多个 type 用 ; 分隔
102
+
103
+ 示例:
104
+ -policy "license:Apache-2.0@.*|MIT@third_party/.*;copyright:Huawei@.*"
105
+ ```
106
+
107
+ ### `-filter` 格式
108
+
109
+ ```
110
+ type:pattern[|type:pattern2 ...]
111
+
112
+ 示例:
113
+ -filter "filename:*.pb.h|filename:*.pb.cc|filepath:build/.*"
114
+ ```
115
+
116
+ ---
117
+
118
+ ## 📋 规则配置(OAT.xml)
119
+
120
+ 工具会按以下优先级合并配置:
121
+
122
+ ```
123
+ 内置 OAT-Default.xml(全局默认规则)
124
+ ↓ 合并
125
+ {-s 目录}/OAT.xml(项目自定义规则,可选)
126
+ ↓ 合并
127
+ 命令行 -policy / -filter(最高优先级)
128
+ ```
129
+
130
+ ### 在被扫描项目中创建 OAT.xml
131
+
132
+ 将 `OAT.xml` 放在被扫描项目的**根目录**,工具会自动加载:
133
+
134
+ ```xml
135
+ <?xml version="1.0" encoding="UTF-8"?>
136
+ <configuration>
137
+ <oatconfig>
138
+
139
+ <!-- 自定义 policy:在默认规则基础上追加 -->
140
+ <policylist>
141
+ <policy name="projectPolicy" desc="">
142
+ <!-- 允许 third_party 目录使用 MIT -->
143
+ <policyitem type="license" name="MIT" path="third_party/.*"
144
+ desc="Third-party MIT licensed code"/>
145
+ <!-- 允许 third_party 目录使用 BSD-3-Clause -->
146
+ <policyitem type="license" name="BSD-3-Clause" path="third_party/.*"
147
+ desc="Third-party BSD licensed code"/>
148
+ <!-- 排除自动生成文件的 copyright 检查 -->
149
+ <policyitem type="copyright" name="*" path="!generated/.*"
150
+ rule="may" desc="Skip generated files"/>
151
+ </policy>
152
+ </policylist>
153
+
154
+ <!-- 自定义 filter:补充不需要扫描的文件 -->
155
+ <filefilterlist>
156
+ <filefilter name="defaultFilter" desc="Project-specific excludes">
157
+ <filteritem type="filepath" name="build/.*" desc="Build output"/>
158
+ <filteritem type="filepath" name="cmake-build/.*" desc="CMake output"/>
159
+ <filteritem type="filename" name="*.pb.h|*.pb.cc" desc="Protobuf generated"/>
160
+ </filefilter>
161
+ </filefilterlist>
162
+
163
+ <!-- 自定义 license 识别规则(识别非 SPDX 标准 license 文本) -->
164
+ <licensematcherlist>
165
+ <licensematcher name="Huawei-Internal">
166
+ <licensetext name="Huawei Proprietary and Confidential"/>
167
+ </licensematcher>
168
+ </licensematcherlist>
169
+
170
+ </oatconfig>
171
+ </configuration>
172
+ ```
173
+
174
+ ### OAT.xml 配置项说明
175
+
176
+ #### `<policyitem>` 字段
177
+
178
+ | 字段 | 必填 | 说明 |
179
+ |------|------|------|
180
+ | `type` | 是 | `license`/`copyright`/`filetype`/`filename`/`compatibility` |
181
+ | `name` | 是 | 匹配值;`*` 匹配所有;`!xxx` 表示"不允许 xxx" |
182
+ | `path` | 是 | 文件路径正则;`!` 前缀表示排除,如 `!test/.*` |
183
+ | `rule` | 否 | `may`(默认,组内任意一条通过即可)或 `must`(必须通过) |
184
+ | `group` | 否 | 同组的 `may` 条目取 OR 逻辑,默认 `defaultGroup` |
185
+ | `filefilter` | 否 | 绑定 filter 名称,默认 `defaultPolicyFilter` |
186
+ | `desc` | 是 | 说明该规则的原因(审查时必填) |
187
+
188
+ #### `<filteritem>` 字段
189
+
190
+ | 字段 | 说明 |
191
+ |------|------|
192
+ | `type="filename"` | 按文件名过滤,支持 `*` 通配符,多个用 `\|` 分隔 |
193
+ | `type="filepath"` | 按文件路径过滤,支持正则表达式 |
194
+
195
+ ---
196
+
197
+ ## 📁 项目结构
198
+
199
+ ```
200
+ oat_python/
201
+ ├── pyproject.toml # 包配置(pip install)
202
+ └── src/
203
+ └── oat/
204
+ ├── __init__.py
205
+ ├── __main__.py # 入口:python -m oat
206
+ ├── cli.py # 命令行解析(argparse)
207
+ ├── config/
208
+ │ ├── schema.py # 数据类:OatConfig / Policy / FileFilter / Project
209
+ │ └── loader.py # 加载 OAT-Default.xml + OAT.xml + CLI 覆盖
210
+ ├── walker/
211
+ │ └── directory_walker.py # 目录遍历 + 文件过滤
212
+ ├── matchers/
213
+ │ ├── license_matcher.py # SPDX / 全文 / 关键词 license 匹配
214
+ │ └── copyright_matcher.py # Copyright 行提取
215
+ ├── analysis/
216
+ │ ├── document.py # FileDocument / Issue 数据类
217
+ │ ├── file_type.py # 二进制 / 归档文件检测
218
+ │ ├── header_reader.py # 读取文件头,驱动 matchers
219
+ │ ├── policy_verifier.py # 对照 policy 校验检测结果
220
+ │ └── pipeline.py # 并发扫描调度(ThreadPoolExecutor)
221
+ ├── reporter/
222
+ │ ├── model.py # 报告数据结构
223
+ │ └── plain_reporter.py # 生成 PlainReport_*.txt
224
+ ├── resources/
225
+ │ ├── OAT-Default.xml # 内置全局默认规则
226
+ │ └── builtin_licenses.json # 50+ 种 license 内置匹配规则
227
+ └── utils/
228
+ └── text_util.py # 文本清洗工具(对齐 Java OatLicenseTextUtil)
229
+ ```
230
+
231
+ ---
232
+
233
+ ## 📊 报告格式
234
+
235
+ 报告输出到 `-r` 指定目录,文件名为 `PlainReport_{-n}.txt`,格式与原版 Java OAT 对齐:
236
+
237
+ ```
238
+ OAT Scan Report
239
+ ===============
240
+ Project : YourRepoName
241
+ ScanDate: 2026-04-09 10:00:00
242
+ ScanDir : /path/to/your/repo
243
+ Files : 120
244
+ Issues : 3
245
+
246
+ [License Header Invalid]
247
+ File: src/foo.cpp
248
+ License: InvalidLicense
249
+ ...
250
+
251
+ [Copyright Header Invalid]
252
+ File: src/bar.cpp
253
+ Copyright: (none)
254
+ ...
255
+ ```
256
+
257
+ ---
258
+
259
+ ## 🔄 与原版 Java OAT 对比
260
+
261
+ | 特性 | Java 版 | Python 版 |
262
+ |------|---------|-----------|
263
+ | 运行环境 | JDK 8+ | Python 3.9+ |
264
+ | 安装方式 | Maven 构建 / 预编译 jar | `pip install` |
265
+ | 扫描速度(143 个文件) | ~15s | **~0.6s** |
266
+ | 项目 OAT.xml 支持 | ✅ | ✅ |
267
+ | OAT-Default.xml 可替换 | ✅(`-oatconfig`) | ✅(`-oatconfig`) |
268
+ | 命令行 policy 覆盖 | ✅ | ✅ |
269
+ | 增量扫描(`-w 1 -f`) | ✅ | ✅ |
270
+ | SPDX license 识别 | ✅ | ✅ |
271
+ | 自定义 licensematcher | ✅ | ✅ |
272
+ | 多项目模式(`-mode p`) | ✅ | ❌(计划支持) |
273
+
274
+ ---
275
+
276
+ ## 🔧 集成到 pre-commit
277
+
278
+ 在被扫描项目的 `.pre-commit-config.yaml` 中添加:
279
+
280
+ ```yaml
281
+ repos:
282
+ - repo: local
283
+ hooks:
284
+ - id: oat-check
285
+ name: OAT License Check
286
+ language: python
287
+ entry: python -m oat
288
+ args:
289
+ - -mode
290
+ - s
291
+ - -s
292
+ - .
293
+ - -r
294
+ - oat_reports/single
295
+ - -n
296
+ - YourRepoName
297
+ - -w
298
+ - "1"
299
+ pass_filenames: false
300
+ additional_dependencies:
301
+ - lxml>=5.0
302
+ - chardet>=5.0
303
+ ```
304
+
305
+ ---
306
+
307
+ ## ❓ 常见问题
308
+
309
+ **Q: 如何自定义 license 检查规则?**
310
+ A: 在项目根目录创建 `OAT.xml`,添加 `<policylist>` 配置,工具自动读取。
311
+
312
+ **Q: 某些文件不需要检查,如何排除?**
313
+ A: 在 `OAT.xml` 的 `<filefilterlist>` 中添加 `<filteritem>`,支持文件名 glob 和路径正则。
314
+
315
+ **Q: 如何识别项目中的自定义 license 文本?**
316
+ A: 在 `OAT.xml` 的 `<licensematcherlist>` 中添加 `<licensetext>`,工具会将匹配到的文本映射为指定 license 名称。
317
+
318
+ **Q: 运行时出现 `ModuleNotFoundError: No module named 'oat'`?**
319
+ A: 使用安装模式:`pip install -e /path/to/oat_python`,或设置 `PYTHONPATH=src`。
320
+
321
+ **Q: 如何跳过项目 OAT.xml 只用全局规则?**
322
+ A: 添加 `-ignorePrjOAT` 参数。
323
+
324
+ ---
325
+
326
+ ## 📞 参考
327
+
328
+ - **原版 Java OAT**: https://gitcode.com/openharmony-sig/tools_oat
329
+ - **SPDX License List**: https://spdx.org/licenses/
330
+ - **OpenHarmony 合规指南**: https://www.openharmony.cn/
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "oat-py"
7
+ version = "1.0.0"
8
+ description = "OSS Audit Tool - Python Edition"
9
+ requires-python = ">=3.9"
10
+ dependencies = [
11
+ "lxml>=5.0",
12
+ "chardet>=5.0",
13
+ ]
14
+
15
+ [project.scripts]
16
+ oat = "oat.__main__:main"
17
+
18
+ [tool.setuptools.packages.find]
19
+ where = ["src"]
20
+ include = ["oat*"]
21
+
22
+ [tool.setuptools.package-data]
23
+ "oat.resources" = ["*.json", "*.xml"]
oat_py-1.0.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ # OAT - OpenHarmony OSS Audit Tool (Python Edition)
2
+ # Copyright (c) 2024 Huawei Device Co., Ltd.
3
+ # Licensed under the Apache License, Version 2.0
4
+ __version__ = "2.0.0"
@@ -0,0 +1,40 @@
1
+ """
2
+ Entry point: python -m oat or oat (if installed via pip)
3
+ """
4
+ import sys
5
+ import time
6
+ from pathlib import Path
7
+
8
+
9
+ def main(argv=None):
10
+ from oat.cli import parse_args
11
+ args = parse_args(argv)
12
+
13
+ start = time.time()
14
+ print(f"[OAT] OpenHarmony OSS Audit Tool - Python Edition")
15
+ print(f"[OAT] Scanning: {args.s}")
16
+
17
+ from oat.config.loader import build_task
18
+ from oat.analysis.pipeline import run_pipeline
19
+ from oat.reporter.plain_reporter import PlainReporter
20
+
21
+ task = build_task(args)
22
+
23
+ report_model = run_pipeline(task)
24
+
25
+ reporter = PlainReporter(task, report_model)
26
+ reporter.write()
27
+
28
+ elapsed = time.time() - start
29
+ total = report_model.total_issues
30
+ print(f"[OAT] Scan complete in {elapsed:.1f}s. Issues found: {total}")
31
+ if total > 0:
32
+ print(f"[OAT] Report: {task.report_dir}")
33
+ sys.exit(1)
34
+ else:
35
+ print("[OAT] No compliance issues found.")
36
+ sys.exit(0)
37
+
38
+
39
+ if __name__ == "__main__":
40
+ main()
File without changes
@@ -0,0 +1,36 @@
1
+ """
2
+ FileDocument — mirrors OatFileDocument.java.
3
+ Holds analysis results for a single file.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+ from typing import List, Optional
10
+
11
+
12
+ @dataclass
13
+ class FileDocument:
14
+ path: Path
15
+ rel_path: str = "" # relative to src_dir, forward slashes
16
+
17
+ # Set by file_type analyser
18
+ file_type: str = "text" # 'text' | 'binary' | 'archive'
19
+
20
+ # Set by header analyser
21
+ license: str = "NoLicenseHeader"
22
+ copyright_owners: List[str] = field(default_factory=list)
23
+
24
+ # Set by policy_verifier
25
+ issues: List["Issue"] = field(default_factory=list)
26
+
27
+ @property
28
+ def has_issues(self) -> bool:
29
+ return bool(self.issues)
30
+
31
+
32
+ @dataclass
33
+ class Issue:
34
+ issue_type: str # 'filetype' | 'license' | 'copyright' | 'filename'
35
+ description: str
36
+ detail: str = ""
@@ -0,0 +1,136 @@
1
+ """
2
+ File type analyser — mirrors OatFileTypeAnalyser.java + Apache Rat BinaryGuesser.
3
+ Detects binary and archive files.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ from pathlib import Path
8
+
9
+ # Archive file extensions (lowercase)
10
+ _ARCHIVE_EXTS = {
11
+ ".jar", ".gz", ".zip", ".tar", ".bz2", ".rar", ".war", ".7z",
12
+ ".rpm", ".deb", ".img", ".apk", ".ipa", ".whl", ".egg",
13
+ ".tar.gz", ".tar.bz2", ".tar.xz", ".tgz", ".tbz2",
14
+ }
15
+
16
+ # Binary/pre-built file extensions (lowercase).
17
+ # Mirrors OatFileUtils.PREBUILD_FILE_EXTENSION + Apache Rat BinaryGuesser extension lists.
18
+ _BINARY_EXTS = {
19
+ ".so", ".dll", ".exe", ".elf", ".bin", ".a", ".o", ".class",
20
+ ".pyc", ".pyd", ".pyo", ".hap", ".scr", ".lib", ".pdb",
21
+ ".obj", ".ko", ".d.ts", ".exp",
22
+ }
23
+
24
+ # Keystore / certificate binary extensions from Apache Rat BinaryGuesser.KEYSTORE_EXTENSIONS.
25
+ # These are always treated as binary regardless of content.
26
+ _KEYSTORE_EXTS = {
27
+ ".jks", ".keystore", ".pem", ".crl", ".truststore", ".cert", ".ks",
28
+ }
29
+
30
+ # Image extensions from Apache Rat BinaryGuesser.IMAGE_EXTENSIONS.
31
+ _IMAGE_EXTS = {
32
+ ".png", ".pdf", ".gif", ".giff", ".tif", ".tiff", ".jpg", ".jpeg",
33
+ ".ico", ".icns", ".psd", ".webp", ".bmp", ".svg",
34
+ }
35
+
36
+ # Audio / media extensions from Apache Rat BinaryGuesser.AUDIO_EXTENSIONS.
37
+ _AUDIO_EXTS = {
38
+ ".aif", ".iff", ".m3u", ".mid", ".mp3", ".mpa", ".wav", ".wma",
39
+ }
40
+
41
+ # Font / layout binary extensions
42
+ _FONT_EXTS = {
43
+ ".woff", ".woff2", ".ttf", ".eot", ".otf",
44
+ }
45
+
46
+ # Extensions that Apache Rat BinaryGuesser.NON_BINARY_EXTENSIONS explicitly lists as text.
47
+ # Files with these extensions are NEVER treated as binary by content-sniffing.
48
+ _NON_BINARY_EXTS = {
49
+ ".ac", ".am", ".bat", ".cat", ".cgi", ".classpath", ".cmd", ".config",
50
+ ".cpp", ".css", ".cwiki", ".data", ".dcl", ".dtd", ".egrm", ".ent",
51
+ ".ft", ".fn", ".fv", ".grm", ".go", ".htaccess", ".html", ".ihtml",
52
+ ".in", ".jmx", ".jsp", ".js", ".json", ".junit", ".jx",
53
+ ".manifest", ".md", ".mf", ".meta", ".mod",
54
+ ".pen", ".pl", ".pm", ".pod", ".pom", ".project", ".properties",
55
+ ".py", ".rb", ".rdf", ".rnc", ".rng", ".rnx", ".roles", ".rss",
56
+ ".sh", ".sql", ".svg", ".tld", ".txt", ".types",
57
+ ".vm", ".vsl", ".wsdd", ".wsdl", ".xargs", ".xcat", ".xconf",
58
+ ".xegrm", ".xgrm", ".xlex", ".xlog", ".xmap", ".xml",
59
+ ".xroles", ".xsamples", ".xsd", ".xsl", ".xslt", ".xsp", ".xul",
60
+ ".xweb", ".xwelcome",
61
+ # Additional common text extensions not in Apache Rat 0.13 but treated as text by OAT
62
+ ".c", ".h", ".cc", ".hh", ".java", ".ts", ".cs", ".rb", ".php",
63
+ ".yml", ".yaml", ".toml", ".rst", ".tex",
64
+ ".gradle", ".cmake", ".mk", ".makefile",
65
+ ".proto", ".thrift", ".avro",
66
+ ".ini", ".cfg", ".conf",
67
+ }
68
+
69
+ # Probe this many bytes for content-based binary detection
70
+ _PROBE_BYTES = 512
71
+
72
+ # Apache Rat HIGH_BYTES_RATIO threshold: if >30% of chars are non-ASCII → binary
73
+ _HIGH_BYTE_THRESHOLD = 30
74
+
75
+
76
+ def is_archive(path: Path) -> bool:
77
+ """Return True if the file is a known archive type."""
78
+ name = path.name.lower()
79
+ # Check compound extensions first
80
+ for ext in (".tar.gz", ".tar.bz2", ".tar.xz", ".so.gz"):
81
+ if name.endswith(ext):
82
+ return True
83
+ return path.suffix.lower() in _ARCHIVE_EXTS
84
+
85
+
86
+ def is_binary(path: Path) -> bool:
87
+ """Return True if the file should be treated as binary.
88
+
89
+ Mirrors Apache Rat BinaryGuesser.isBinary() logic used by OAT Java:
90
+ 1. Known binary extensions → binary
91
+ 2. Known keystore/image/audio/font extensions → binary
92
+ 3. Explicitly listed non-binary extensions → NOT binary (skip content check)
93
+ 4. Content check: if >30% of bytes in first 512 bytes are >127 → binary
94
+ 5. Null byte in first 512 bytes → binary
95
+ """
96
+ ext = path.suffix.lower()
97
+ name_lower = path.name.lower()
98
+
99
+ # Step 1: known binary extensions
100
+ if ext in _BINARY_EXTS:
101
+ return True
102
+
103
+ # Step 2: keystore / image / audio / font → binary
104
+ if ext in _KEYSTORE_EXTS or ext in _IMAGE_EXTS or ext in _AUDIO_EXTS or ext in _FONT_EXTS:
105
+ return True
106
+
107
+ # Step 3: explicitly non-binary extension → skip content check
108
+ if ext in _NON_BINARY_EXTS:
109
+ return False
110
+
111
+ # Step 4+5: content-based detection
112
+ try:
113
+ with open(path, "rb") as f:
114
+ chunk = f.read(_PROBE_BYTES)
115
+ if not chunk:
116
+ return False
117
+ # Null byte → binary
118
+ if b"\x00" in chunk:
119
+ return True
120
+ # High-byte ratio check (mirrors Apache Rat BinaryGuesser HIGH_BYTES_RATIO=30)
121
+ high = sum(1 for b in chunk if b > 127)
122
+ if high * 100 // len(chunk) > _HIGH_BYTE_THRESHOLD:
123
+ return True
124
+ except OSError:
125
+ pass
126
+
127
+ return False
128
+
129
+
130
+ def get_file_type(path: Path) -> str:
131
+ """Return 'archive', 'binary', or 'text'."""
132
+ if is_archive(path):
133
+ return "archive"
134
+ if is_binary(path):
135
+ return "binary"
136
+ return "text"