@djordje-stojanovic/sigmaskills 0.9.0 → 0.9.2
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.
- package/CHANGELOG.md +15 -1
- package/README.md +1 -1
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/sigmacreatearena/SKILL.md +1 -1
- package/sigmadeeplook/references/installs.md +1 -1
- package/sigmadeeplook/scripts/dl_3d.py +31 -1
- package/sigmadeeplook/scripts/dl_adapt.py +15 -2
- package/sigmadeeplook/scripts/dl_bins.py +245 -11
- package/sigmadeeplook/scripts/dl_binx.py +2 -1
- package/sigmadeeplook/scripts/dl_cls.py +289 -0
- package/sigmadeeplook/scripts/dl_core.py +28 -0
- package/sigmadeeplook/scripts/dl_data.py +11 -2
- package/sigmadeeplook/scripts/dl_docs.py +27 -1
- package/sigmadeeplook/scripts/dl_extra.py +2 -0
- package/sigmadeeplook/scripts/dl_listcol.py +99 -0
- package/sigmadeeplook/scripts/dl_media.py +16 -0
- package/sigmadeeplook/scripts/dl_misc.py +593 -0
- package/sigmadeeplook/scripts/dl_reg.py +120 -0
- package/sigmadeeplook/scripts/dl_tab2.py +249 -0
- package/sigmadeeplook/scripts/dl_vba.py +220 -0
- package/sigmadeeplook/scripts/dl_write.py +20 -2
- package/sigmadeeplook/scripts/dl_xlcalc.py +272 -0
- package/sigmadeeplook/scripts/look.py +3 -3
- package/sigmafreedom/scripts/guardrail_allowlist.txt +3 -0
- package/sigmaresearch/SKILL.md +1 -1
- package/sigmawritegui/SKILL.md +1 -1
- package/src/catalog.js +10 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ All notable changes to [Sigmaskills](https://github.com/Djordje-Stojanovic/Sigma
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.9.2] — 2026-10-18
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- `sigmadeeplook` readers from the held-out rounds: binaries, archives, tables and spreadsheets read more fields correctly (typed binary fields, VBA source, `.reg`, `.deb` members, list columns, MIDI events, docx Title) (#111, #112). The headerless-records test now skips when numpy is missing, which turns the main branch CI green again.
|
|
12
|
+
|
|
13
|
+
## [0.9.1] — 2026-10-18
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- `sigmacreatearena`, `sigmaresearch` and `sigmawritegui` had a plain YAML description containing `: `, which strict YAML parsers reject. The descriptions are now quoted, the installer reads quoted values as plain text, and a test keeps every header strict YAML.
|
|
18
|
+
|
|
7
19
|
## [0.9.0] — 2026-10-18
|
|
8
20
|
|
|
9
21
|
### Changed
|
|
@@ -269,7 +281,9 @@ Ship high-rigor, installable agent skills (not apps): full-repo audits, performa
|
|
|
269
281
|
| 2026-08-11 | `5dab832` | Add CHANGELOG and mark v0.1.0 as the first Sigmaskills release |
|
|
270
282
|
| 2026-08-11 | `124a666` | Polish v0.1.0 docs for multi-host install and use |
|
|
271
283
|
|
|
272
|
-
[Unreleased]: https://github.com/Djordje-Stojanovic/Sigmaskills/compare/v0.9.
|
|
284
|
+
[Unreleased]: https://github.com/Djordje-Stojanovic/Sigmaskills/compare/v0.9.2...HEAD
|
|
285
|
+
[0.9.2]: https://github.com/Djordje-Stojanovic/Sigmaskills/compare/v0.9.1...v0.9.2
|
|
286
|
+
[0.9.1]: https://github.com/Djordje-Stojanovic/Sigmaskills/compare/v0.9.0...v0.9.1
|
|
273
287
|
[0.9.0]: https://github.com/Djordje-Stojanovic/Sigmaskills/compare/v0.8.0...v0.9.0
|
|
274
288
|
[0.8.0]: https://github.com/Djordje-Stojanovic/Sigmaskills/compare/v0.7.0...v0.8.0
|
|
275
289
|
[0.7.0]: https://github.com/Djordje-Stojanovic/Sigmaskills/compare/v0.6.1...v0.7.0
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Portable [Agent Skills](https://agentskills.io/) that install once and run on **
|
|
|
6
6
|
|
|
7
7
|
| | |
|
|
8
8
|
|---|---|
|
|
9
|
-
| **Release** | [**v0.9.
|
|
9
|
+
| **Release** | [**v0.9.2**](https://github.com/Djordje-Stojanovic/Sigmaskills/releases/tag/v0.9.2) |
|
|
10
10
|
| **Changelog** | [CHANGELOG.md](CHANGELOG.md) |
|
|
11
11
|
| **License** | [MIT](LICENSE) |
|
|
12
12
|
| **Spec** | [agentskills.io](https://agentskills.io/) |
|
package/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sigmacreatearena
|
|
3
|
-
description: Builds a fast blind-pairs arena so the user ranks any options by Elo per metric: models, settings, voices, captions, thumbnails, outfits. Grills first, then ships a local page and a board. Use to rank, compare or pick the best of many options by taste. Do not use for 2-3 options (just ask), for measurable checks (script them), or to judge one draft against a reference.
|
|
3
|
+
description: 'Builds a fast blind-pairs arena so the user ranks any options by Elo per metric: models, settings, voices, captions, thumbnails, outfits. Grills first, then ships a local page and a board. Use to rank, compare or pick the best of many options by taste. Do not use for 2-3 options (just ask), for measurable checks (script them), or to judge one draft against a reference.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# SigmaCreateArena
|
|
@@ -45,7 +45,7 @@ Every row below is a 'Not looked at' row when the tool is absent. Nothing needs
|
|
|
45
45
|
| cairosvg | python package | rendering svg | pip install cairosvg |
|
|
46
46
|
| rapidocr_onnxruntime | python package | OCR of video frames, images, scanned pages | pip install rapidocr-onnxruntime |
|
|
47
47
|
| lief | python package | real Authenticode verification (chain and hash), imphash | pip install lief |
|
|
48
|
-
| oletools | python package | VBA
|
|
48
|
+
| oletools | python package | fuller VBA analysis (the stdlib reader already decompresses the modules, lists auto-run routines, suspicious calls and strings) | pip install oletools |
|
|
49
49
|
| python_calamine | python package | very large xlsx files | pip install python-calamine |
|
|
50
50
|
| dnfile | python package | .NET assembly names, references, types | pip install dnfile |
|
|
51
51
|
| pymupdf4llm | python package | (reserved) PDF to markdown with tables | pip install pymupdf4llm |
|
|
@@ -120,7 +120,14 @@ def look_3d(look):
|
|
|
120
120
|
look.flag("PARSE-ERROR", "glTF", f"{type(e).__name__}: {e}"[:100], "")
|
|
121
121
|
elif st in ("stl", "stl-ascii"):
|
|
122
122
|
tris = parse_stl(b, st == "stl-ascii")
|
|
123
|
-
|
|
123
|
+
hdr = ""
|
|
124
|
+
if st != "stl-ascii":
|
|
125
|
+
hdr = b[:80].split(b"\0")[0].decode("latin-1").strip()
|
|
126
|
+
hdr = f" Header text: {hdr!r}." if hdr else " Empty 80-byte header."
|
|
127
|
+
else:
|
|
128
|
+
first = b[:200].decode("latin-1").splitlines()[0].strip()
|
|
129
|
+
hdr = f" First line: {first!r}."
|
|
130
|
+
look.line(f"STL ({'ASCII' if st == 'stl-ascii' else 'binary'}): {len(tris)} triangles.{hdr} STL has no units and no colour.")
|
|
124
131
|
elif st == "obj":
|
|
125
132
|
tris, c = parse_obj(b.decode("utf-8", "replace"))
|
|
126
133
|
look.fact("vertices", c["v"])
|
|
@@ -158,6 +165,8 @@ def look_3d(look):
|
|
|
158
165
|
look.fact("triangles", len(tris))
|
|
159
166
|
look.fact("bbox", stats["bbox"])
|
|
160
167
|
look.line(f"{len(tris)} triangles; bounding box {stats['bbox']['min']} to {stats['bbox']['max']} (size {stats['bbox']['size']}).")
|
|
168
|
+
look.line(f"Mesh measures: surface area {stats['surface_area']:,.3f}, signed volume {stats['signed_volume']:,.3f} (a true volume only when watertight), "
|
|
169
|
+
+ (f"centroid {stats['centroid']}, " if stats.get("centroid") else "") + f"{stats['unique_vertices']} unique vertices, {stats['degenerate_triangles']} zero-area triangles (units are those of the file).", 61)
|
|
161
170
|
if stats.get("watertight") is not None:
|
|
162
171
|
look.fact("watertight", stats["watertight"])
|
|
163
172
|
look.line("Watertight (every edge shared by exactly two triangles): " + ("yes" if stats["watertight"] else f"NO ({stats['open_edges']} open edges)"))
|
|
@@ -257,6 +266,27 @@ def mesh_stats(look, tris):
|
|
|
257
266
|
mn = [round(min(xs), 4), round(min(ys), 4), round(min(zs), 4)]
|
|
258
267
|
mx = [round(max(xs), 4), round(max(ys), 4), round(max(zs), 4)]
|
|
259
268
|
out = {"triangles": len(tris), "bbox": {"min": mn, "max": mx, "size": [round(a - b, 4) for a, b in zip(mx, mn)]}}
|
|
269
|
+
area = vol = 0.0
|
|
270
|
+
cx = cy = cz = 0.0
|
|
271
|
+
degenerate = 0
|
|
272
|
+
for a, b, c in tris:
|
|
273
|
+
ux, uy, uz = b[0] - a[0], b[1] - a[1], b[2] - a[2]
|
|
274
|
+
vx, vy, vz = c[0] - a[0], c[1] - a[1], c[2] - a[2]
|
|
275
|
+
nx, ny, nz = uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx
|
|
276
|
+
ar = 0.5 * math.sqrt(nx * nx + ny * ny + nz * nz)
|
|
277
|
+
if ar < 1e-12:
|
|
278
|
+
degenerate += 1
|
|
279
|
+
area += ar
|
|
280
|
+
d = (a[0] * (b[1] * c[2] - b[2] * c[1]) - a[1] * (b[0] * c[2] - b[2] * c[0]) + a[2] * (b[0] * c[1] - b[1] * c[0])) / 6.0
|
|
281
|
+
vol += d
|
|
282
|
+
cx += d * (a[0] + b[0] + c[0]) / 4.0
|
|
283
|
+
cy += d * (a[1] + b[1] + c[1]) / 4.0
|
|
284
|
+
cz += d * (a[2] + b[2] + c[2]) / 4.0
|
|
285
|
+
out["surface_area"] = round(area, 4)
|
|
286
|
+
out["signed_volume"] = round(vol, 4)
|
|
287
|
+
out["centroid"] = [round(cx / vol, 4), round(cy / vol, 4), round(cz / vol, 4)] if abs(vol) > 1e-12 else None
|
|
288
|
+
out["degenerate_triangles"] = degenerate
|
|
289
|
+
out["unique_vertices"] = len({(round(v[0] * 1e5), round(v[1] * 1e5), round(v[2] * 1e5)) for t in tris for v in t}) if len(tris) <= MAX_TRIS_PARSE else None
|
|
260
290
|
if len(tris) <= MAX_TRIS_PARSE:
|
|
261
291
|
q = lambda v: (round(v[0] * 1e5), round(v[1] * 1e5), round(v[2] * 1e5))
|
|
262
292
|
edges = {}
|
|
@@ -108,6 +108,15 @@ def look_avro(look):
|
|
|
108
108
|
break
|
|
109
109
|
look.write_text("sample.json", json.dumps(rows, indent=1, default=str)[:20000], "the first 5 records", "json", value=6)
|
|
110
110
|
look.step("sample records", "fastavro")
|
|
111
|
+
if not look.quick and records:
|
|
112
|
+
import dl_data as D
|
|
113
|
+
names = [n for n, _t in fields]
|
|
114
|
+
|
|
115
|
+
def rowsgen():
|
|
116
|
+
with open(look.path, "rb") as f2:
|
|
117
|
+
for k, rec in enumerate(fa.reader(f2), 1):
|
|
118
|
+
yield k, [json.dumps(rec.get(n), default=str, sort_keys=True) if isinstance(rec.get(n), (list, dict)) else rec.get(n) for n in names]
|
|
119
|
+
D.table_profile(look, "Records", names, rowsgen, "record", records, 84, 28)
|
|
111
120
|
except Exception as e:
|
|
112
121
|
look.fail("record values", "fastavro", e)
|
|
113
122
|
|
|
@@ -327,8 +336,10 @@ def summarize_olevba(results):
|
|
|
327
336
|
|
|
328
337
|
def olevba_facts(look, data, name):
|
|
329
338
|
m = look.mod("oletools")
|
|
330
|
-
if not m:
|
|
331
|
-
|
|
339
|
+
if not m: # the standard-library reader: decompresses the VBA modules itself
|
|
340
|
+
import dl_vba
|
|
341
|
+
if not dl_vba.vba_facts(look, data, name):
|
|
342
|
+
look.skip("macro source: full oletools analysis", "python package oletools not importable" if not look.no_tools else "disabled by --no-tools", "oletools")
|
|
332
343
|
return
|
|
333
344
|
try:
|
|
334
345
|
from oletools.olevba import VBA_Parser
|
|
@@ -348,6 +359,8 @@ def olevba_facts(look, data, name):
|
|
|
348
359
|
look.step("macro analysis", "oletools")
|
|
349
360
|
except Exception as e:
|
|
350
361
|
look.fail("macro analysis", "oletools", e)
|
|
362
|
+
import dl_vba
|
|
363
|
+
dl_vba.vba_facts(look, data, name)
|
|
351
364
|
|
|
352
365
|
|
|
353
366
|
# ================================================================= glTF validator
|
|
@@ -14,6 +14,7 @@ import os
|
|
|
14
14
|
import re
|
|
15
15
|
import shutil
|
|
16
16
|
import struct
|
|
17
|
+
import subprocess
|
|
17
18
|
import tarfile
|
|
18
19
|
import time
|
|
19
20
|
import zipfile
|
|
@@ -55,9 +56,16 @@ def look_unknown(look):
|
|
|
55
56
|
look.skip("file -b type guess", "`file` not found" if not look.no_tools else "disabled by --no-tools", "file", "install the `file` utility (Git for Windows ships it)")
|
|
56
57
|
if b:
|
|
57
58
|
look.write_text("hexdump.txt", core.hexdump(b[:512]), "first 512 bytes as hex", "internal")
|
|
58
|
-
|
|
59
|
+
if b and look.size == len(b) or (b and look.size <= (64 << 20)):
|
|
60
|
+
import dl_misc
|
|
61
|
+
for t in dl_misc.record_layout(read_bytes(look, 64 << 20), look.size):
|
|
62
|
+
look.headline = "Headerless binary file that looks like a table of fixed-size records (a guess from the bytes)"
|
|
63
|
+
look.line(t, 93)
|
|
64
|
+
if b:
|
|
65
|
+
look.line("First 32 bytes: " + b[:32].hex(" ") + " | " + "".join(chr(c) if 32 <= c < 127 else "." for c in b[:32]), 52)
|
|
66
|
+
st = [x for x in core.strings_of(b, 6, 4000) if wordy(x[2])]
|
|
59
67
|
if st:
|
|
60
|
-
look.write_text("strings.txt", "\n".join(f"{o:08x} {s[:200]}" for o, k, s in st), "printable strings (min
|
|
68
|
+
look.write_text("strings.txt", "\n".join(f"{o:08x} {s[:200]}" for o, k, s in st), "printable strings that read as words, paths or names (min 6)")
|
|
61
69
|
look.ask_human("What is this file meant to be, and where did it come from?")
|
|
62
70
|
|
|
63
71
|
|
|
@@ -438,7 +446,12 @@ def parse_pe(b):
|
|
|
438
446
|
end = b.find(b"\0", no)
|
|
439
447
|
imports.append(b[no:end].decode("latin-1"))
|
|
440
448
|
p += 20
|
|
441
|
-
|
|
449
|
+
base = struct.unpack_from("<Q", b, opt + 24)[0] if magic == 0x20B else struct.unpack_from("<I", b, opt + 28)[0]
|
|
450
|
+
osv = struct.unpack_from("<HH", b, opt + 40)
|
|
451
|
+
soi, _hs, csum, _sub, dllc = struct.unpack_from("<IIIHH", b, opt + 56)
|
|
452
|
+
dllflags = [n for bit, n in ((0x40, "ASLR"), (0x20, "high-entropy ASLR"), (0x100, "DEP/NX"), (0x4000, "control-flow guard"), (0x400, "no SEH"), (0x1000, "AppContainer")) if dllc & bit]
|
|
453
|
+
return {"image_base": base, "image_size": soi, "checksum": csum, "linker": f"{b[opt + 2]}.{b[opt + 3]}", "os_version": f"{osv[0]}.{osv[1]}", "mitigations": dllflags, "characteristics": chars,
|
|
454
|
+
"machine": PE_MACHINES.get(machine, hex(machine)), "timestamp": ts, "subsystem": SUBSYS.get(subsystem, str(subsystem)),
|
|
442
455
|
"pe32plus": magic == 0x20B, "sections": secs, "imports": imports, "dll": bool(chars & 0x2000), "entry_rva": entry,
|
|
443
456
|
"exports": exports, "export_count": export_count, "signed": bool(sec_off and sec_sz), "version": version}
|
|
444
457
|
|
|
@@ -481,6 +494,10 @@ def look_binary(look):
|
|
|
481
494
|
look.headline = f"Windows {kind} ({info['machine']})" + (f", {info['version']['FileDescription']}" if info["version"].get("FileDescription") else "") + (f", {info['export_count']} exports" if info["export_count"] else "")
|
|
482
495
|
look.dims = f"{info['machine']}"
|
|
483
496
|
look.line(f"Windows PE ({kind}): machine {info['machine']}, subsystem {info['subsystem']}." + (f" Exports {info['export_count']} functions, e.g. " + ", ".join(info["exports"][:5]) + "." if info["export_count"] else " No exports."), 80)
|
|
497
|
+
look.line(f"PE header: entry point RVA 0x{info['entry_rva']:x}, image base 0x{info['image_base']:x}, image size {info['image_size']:,} B, {len(info['sections'])} sections, "
|
|
498
|
+
f"linker {info['linker']}, minimum OS {info['os_version']}, mitigations: {', '.join(info['mitigations']) or 'none'}, header checksum {'set' if info['checksum'] else 'zero'}; "
|
|
499
|
+
f"{len(info['imports'])} imported DLLs.", 76)
|
|
500
|
+
look.line("Section table (name, virtual size, raw size, rights): " + "; ".join(f"{x['name']} {x['vsize']:,} / {x['raw']:,} {x['perms']}" for x in info["sections"][:16]) + ".", 47)
|
|
484
501
|
if info["version"]:
|
|
485
502
|
look.line("Version info: " + "; ".join(f"{k} {v}" for k, v in info["version"].items()), 78)
|
|
486
503
|
import dl_pe
|
|
@@ -514,9 +531,19 @@ def look_binary(look):
|
|
|
514
531
|
look.line(f"Mach-O: cpu {MACHO_CPU.get(cpu, hex(cpu))}.")
|
|
515
532
|
look.fact("arch", MACHO_CPU.get(cpu, hex(cpu)))
|
|
516
533
|
elif st == "wasm":
|
|
517
|
-
|
|
534
|
+
import dl_cls
|
|
535
|
+
w = dl_cls.parse_wasm(b)
|
|
536
|
+
look.headline = f"WebAssembly module: {len(w['imports'])} imports, {len(w['exports'])} exports, {w['imported_funcs'] + w['functions']} functions"
|
|
537
|
+
for k, t in enumerate(dl_cls.wasm_lines(w)):
|
|
538
|
+
look.line(t, 97 - k)
|
|
539
|
+
look.fact("exports", [n for n, _ in w["exports"]])
|
|
518
540
|
elif st == "java-class":
|
|
519
|
-
|
|
541
|
+
import dl_cls
|
|
542
|
+
c = dl_cls.parse_class(b)
|
|
543
|
+
look.headline = f"Java class {c['name']}: {len(c['methods'])} methods, {len(c['fields'])} fields, Java {c['release']}"
|
|
544
|
+
for k, t in enumerate(dl_cls.class_lines(c)):
|
|
545
|
+
look.line(t, 97 - k)
|
|
546
|
+
look.fact("methods", [m["name"] for m in c["methods"]])
|
|
520
547
|
elif st == "dex":
|
|
521
548
|
look.line("Android Dalvik executable (dex).")
|
|
522
549
|
else:
|
|
@@ -524,7 +551,7 @@ def look_binary(look):
|
|
|
524
551
|
except (struct.error, ValueError, IndexError) as e:
|
|
525
552
|
look.flag("PARSE-ERROR", "header", str(e)[:100], "the header is truncated or malformed; strings and entropy still ran")
|
|
526
553
|
look.fact("entropy", round(core.entropy(b[:4 << 20]), 3))
|
|
527
|
-
st_list = core.strings_of(b, 8, STRINGS_CAP)
|
|
554
|
+
st_list = [x for x in core.strings_of(b, 8, STRINGS_CAP) if wordy(x[2])] # machine-code bytes that happen to be printable are not strings
|
|
528
555
|
lines = [f"{o:08x} {k} {s[:300]}" for o, k, s in st_list]
|
|
529
556
|
if lines:
|
|
530
557
|
look.write_text("strings.txt", "\n".join(lines), f"ASCII and UTF-16LE strings (min 8, {len(lines)} shown, capped at {STRINGS_CAP})", value=-5)
|
|
@@ -547,6 +574,19 @@ def look_binary(look):
|
|
|
547
574
|
look.ask_human("May the strings or the file be uploaded anywhere? Default: no.", "Which function or behaviour do you need explained?")
|
|
548
575
|
|
|
549
576
|
|
|
577
|
+
_WORD = re.compile(r"[a-z]{4,}|[A-Za-z]{3,}[_./\:-][A-Za-z]{2,}|[A-Z][A-Z0-9]*_[A-Z0-9_]{3,}|https?://|\d{1,4}[.:/-]\d{1,4}[.:/-]\d{1,4}")
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def wordy(s):
|
|
581
|
+
"""True when a printable run reads like text (a word, a path, a constant name, a version or date) rather than code bytes."""
|
|
582
|
+
s = s.strip()
|
|
583
|
+
if len(s) < 6:
|
|
584
|
+
return False
|
|
585
|
+
if not _WORD.search(s):
|
|
586
|
+
return False
|
|
587
|
+
return sum(c.isalnum() or c in " _./\\:-,'\"()%=@#<>[]{}$&*+?!;" for c in s) / len(s) >= 0.85
|
|
588
|
+
|
|
589
|
+
|
|
550
590
|
# ================================================================= archives
|
|
551
591
|
RESERVED = {"CON", "PRN", "AUX", "NUL", *(f"COM{i}" for i in range(1, 10)), *(f"LPT{i}" for i in range(1, 10))}
|
|
552
592
|
MAX_MEMBERS = 10000
|
|
@@ -639,7 +679,7 @@ def screen_members(look, members, depth):
|
|
|
639
679
|
seen.add(key)
|
|
640
680
|
if m["packed"] and m["size"] / max(1, m["packed"]) > MAX_RATIO:
|
|
641
681
|
refusals.append(("ratio", m["name"], f"ratio {m['size'] // max(1, m['packed'])}:1 (limit {MAX_RATIO}:1)"))
|
|
642
|
-
if m["packed"]:
|
|
682
|
+
if m["packed"] and m.get("offset") is not None: # a 7-Zip listing gives no offsets: nothing to compare
|
|
643
683
|
ranges.append((m["offset"], m["offset"] + m["packed"], m["name"]))
|
|
644
684
|
ranges.sort()
|
|
645
685
|
for a, b in zip(ranges, ranges[1:]):
|
|
@@ -730,7 +770,7 @@ def archive_overview(members):
|
|
|
730
770
|
out.append("Largest: " + "; ".join(f"{m['name']} ({core.human(m['size'])})" for m in big))
|
|
731
771
|
nested = [m["name"] for m in files if m["name"].lower().endswith(NESTED_EXT)]
|
|
732
772
|
if nested:
|
|
733
|
-
out.append(f"{len(nested)} members are archives themselves
|
|
773
|
+
out.append(f"{len(nested)} members are archives themselves, e.g. {', '.join(nested[:2])}.")
|
|
734
774
|
risky = [m["name"] for m in files if m["name"].lower().endswith(RISKY_EXT)]
|
|
735
775
|
if risky:
|
|
736
776
|
out.append(f"{len(risky)} runnable-looking members (never run), e.g. {', '.join(risky[:3])}.")
|
|
@@ -766,9 +806,70 @@ def member_reader(look, fmt):
|
|
|
766
806
|
return None
|
|
767
807
|
return t.extractfile(m).read(cap + 1)
|
|
768
808
|
return read, t.close
|
|
809
|
+
sz = look.tool("7z")
|
|
810
|
+
if sz:
|
|
811
|
+
def read(name, cap): # streamed to memory: `7z e -so` writes nothing to disk
|
|
812
|
+
r = subprocess.run([sz, "e", "-so", "-y", str(look.path), name], capture_output=True, timeout=40)
|
|
813
|
+
return r.stdout[:cap + 1] if r.returncode == 0 else None
|
|
814
|
+
return read, (lambda: None)
|
|
769
815
|
return None, (lambda: None)
|
|
770
816
|
|
|
771
817
|
|
|
818
|
+
NEST_OPEN = (".zip", ".jar", ".war", ".whl", ".nupkg", ".tar", ".tgz", ".tar.gz", ".tar.xz", ".tar.bz2", ".txz", ".tbz2")
|
|
819
|
+
SMALL_NOTE = 600 # a text member this short is quoted whole
|
|
820
|
+
SMALL_ANY = 65536 # a member this small is read whatever its name: a file with no extension is often text
|
|
821
|
+
INNER_NEST_MAX = 48 << 20 # a nested archive up to this size is opened in memory
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def _is_textname(name):
|
|
825
|
+
import dl_prof as P
|
|
826
|
+
return P.is_text_name(name) or P.is_manifest(name)
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
def nested_items(b, outer, depth, budget):
|
|
830
|
+
"""Members of an archive held in memory -> (name, size, text or None); one more level for archives inside it (depth 2 at most).
|
|
831
|
+
Reads only text-like members of up to 4 MB; budget[0] counts the bytes read so far over the whole look."""
|
|
832
|
+
out = []
|
|
833
|
+
try:
|
|
834
|
+
if zipfile.is_zipfile(io.BytesIO(b)):
|
|
835
|
+
z = zipfile.ZipFile(io.BytesIO(b))
|
|
836
|
+
infos = [i for i in z.infolist() if not i.is_dir()][:5000]
|
|
837
|
+
|
|
838
|
+
def get(i):
|
|
839
|
+
return None if i.flag_bits & 1 else z.open(i).read(INNER_TEXT_MAX + 1)
|
|
840
|
+
ent = [(i.filename, i.file_size, i) for i in infos]
|
|
841
|
+
else:
|
|
842
|
+
t = tarfile.open(fileobj=io.BytesIO(b), mode="r:*")
|
|
843
|
+
ms = [m for m in t.getmembers() if m.isfile()][:5000]
|
|
844
|
+
|
|
845
|
+
def get(m):
|
|
846
|
+
return t.extractfile(m).read(INNER_TEXT_MAX + 1)
|
|
847
|
+
ent = [(m.name, m.size, m) for m in ms]
|
|
848
|
+
except Exception:
|
|
849
|
+
return out
|
|
850
|
+
for name, size, h in ent:
|
|
851
|
+
text = None
|
|
852
|
+
if depth < 2 and name.lower().endswith(NEST_OPEN) and size <= INNER_NEST_MAX and budget[0] + size <= INNER_TOTAL_MAX:
|
|
853
|
+
try:
|
|
854
|
+
nb = get(h) if size <= INNER_TEXT_MAX else None
|
|
855
|
+
except Exception:
|
|
856
|
+
nb = None
|
|
857
|
+
if nb is not None:
|
|
858
|
+
budget[0] += len(nb)
|
|
859
|
+
for n2, s2, t2 in nested_items(nb, name, depth + 1, budget):
|
|
860
|
+
out.append((f"{name}!/{n2}", s2, t2))
|
|
861
|
+
elif size <= INNER_TEXT_MAX and (_is_textname(name) or 0 < size <= SMALL_ANY) and budget[0] + size <= INNER_TOTAL_MAX:
|
|
862
|
+
try:
|
|
863
|
+
bb = get(h)
|
|
864
|
+
except Exception:
|
|
865
|
+
bb = None
|
|
866
|
+
if bb is not None:
|
|
867
|
+
budget[0] += len(bb)
|
|
868
|
+
text, _enc = core.decode_text(bb)
|
|
869
|
+
out.append((name, size, text))
|
|
870
|
+
return out
|
|
871
|
+
|
|
872
|
+
|
|
772
873
|
def archive_contents(look, members, fmt):
|
|
773
874
|
"""What is inside, read in memory: tree stats (lines, languages, tests, env vars, constants), every manifest / README / licence /
|
|
774
875
|
changelog, and a field profile of each table (CSV, TSV, JSON, JSONL) member. Nothing is extracted to disk."""
|
|
@@ -778,11 +879,28 @@ def archive_contents(look, members, fmt):
|
|
|
778
879
|
files = [m for m in members if m["kind"] == "file"]
|
|
779
880
|
used, t0 = 0, time.time()
|
|
780
881
|
texts = {}
|
|
882
|
+
opened, small = [], {}
|
|
781
883
|
try:
|
|
782
884
|
for m in files:
|
|
783
885
|
name, size = m["name"], m["size"]
|
|
784
886
|
text = None
|
|
785
|
-
|
|
887
|
+
if read and not m["enc"] and name.lower().endswith(NEST_OPEN) and size <= INNER_NEST_MAX and used + size <= INNER_TOTAL_MAX and time.time() - t0 < 60:
|
|
888
|
+
try:
|
|
889
|
+
nb = read(name, INNER_NEST_MAX)
|
|
890
|
+
except Exception:
|
|
891
|
+
nb = None
|
|
892
|
+
if nb is not None and len(nb) <= INNER_NEST_MAX:
|
|
893
|
+
used += len(nb)
|
|
894
|
+
n_in = 0
|
|
895
|
+
for iname, isize, itext in nested_items(nb, name, 1, [used]):
|
|
896
|
+
tree.add(f"{name}!/{iname}", isize, itext)
|
|
897
|
+
n_in += 1
|
|
898
|
+
if itext is not None and len(itext) <= SMALL_NOTE and not P.is_manifest(iname):
|
|
899
|
+
small[f"{name}!/{iname}"] = itext
|
|
900
|
+
if itext is not None and (P.is_manifest(iname) or iname.lower().endswith((".csv", ".tsv", ".json", ".jsonl", ".ndjson", ".yaml", ".yml", ".toml", ".xml", ".properties", ".ini", ".cfg", ".txt", ".md"))):
|
|
901
|
+
texts[f"{name}!/{iname}"] = itext
|
|
902
|
+
opened.append(f"{name} ({n_in} inner files)")
|
|
903
|
+
want = read and not m["enc"] and (P.is_text_name(name) or P.is_manifest(name) or 0 < size <= SMALL_ANY) and size <= max(INNER_TEXT_MAX, INNER_TABLE_MAX if name.lower().endswith((".csv", ".tsv", ".json", ".jsonl", ".ndjson")) else 0)
|
|
786
904
|
if want and used + size <= INNER_TOTAL_MAX and time.time() - t0 < 60:
|
|
787
905
|
try:
|
|
788
906
|
b = read(name, max(INNER_TEXT_MAX, INNER_TABLE_MAX))
|
|
@@ -791,11 +909,18 @@ def archive_contents(look, members, fmt):
|
|
|
791
909
|
if b is not None:
|
|
792
910
|
used += len(b)
|
|
793
911
|
text, _enc = core.decode_text(b)
|
|
912
|
+
if text is not None and len(text) <= SMALL_NOTE and not P.is_manifest(name):
|
|
913
|
+
small[name] = text
|
|
794
914
|
if text is not None and (P.is_manifest(name) or name.lower().endswith((".csv", ".tsv", ".json", ".jsonl", ".ndjson", ".yaml", ".yml", ".toml", ".xml", ".properties", ".ini", ".cfg"))):
|
|
795
915
|
texts[name] = text
|
|
796
916
|
tree.add(name, size, text)
|
|
797
917
|
finally:
|
|
798
918
|
close()
|
|
919
|
+
for n, t in list(small.items())[:20]:
|
|
920
|
+
if t.strip():
|
|
921
|
+
look.line(f"Small text member {n}: " + " | ".join(x.strip() for x in t.strip().splitlines()[:8])[:SMALL_NOTE], 64, "small")
|
|
922
|
+
if opened:
|
|
923
|
+
look.line("Archives inside this archive, opened in memory (nested names are written outer!/inner): " + "; ".join(opened[:10]) + ".", 91)
|
|
799
924
|
tree.emit(look, "Contents", 90, [("members", len(members)), ("folders", sum(1 for m in members if m["kind"] == "dir") or None),
|
|
800
925
|
("encrypted members", sum(1 for m in members if m["enc"]) or None), ("bytes read to look inside", used)])
|
|
801
926
|
# member dates
|
|
@@ -818,6 +943,57 @@ def archive_contents(look, members, fmt):
|
|
|
818
943
|
look.line(f"Config member {n} ({len(t.splitlines())} lines): " + " | ".join(P.short(x, 80) for x in kv), 40, "config")
|
|
819
944
|
|
|
820
945
|
|
|
946
|
+
MEMBER_LOOK_EXT = {".xlsx", ".xlsm", ".xls", ".ods", ".docx", ".doc", ".odt", ".pptx", ".odp", ".pdf", ".sqlite", ".sqlite3", ".db", ".parquet", ".avro", ".orc", ".npz", ".npy", ".dta",
|
|
947
|
+
".mid", ".ttf", ".otf", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".eml", ".ics", ".vcf", ".torrent", ".plist", ".pyc", ".class", ".wasm", ".exe", ".dll", ".deb", ".stl",
|
|
948
|
+
".glb", ".log", ".mp3", ".wav", ".flac", ".ogg", ".opus", ".m4a", ".mp4", ".mkv", ".mov", ".avi", ".webm", ".epub", ".duckdb", ".pcap", ".evtx", ".ipynb"}
|
|
949
|
+
MEMBER_LOOK_N = 8 # members of a document or media type that get a look of their own, in place
|
|
950
|
+
MEMBER_LOOK_BYTES = 40 << 20
|
|
951
|
+
MEMBER_LOOK_TOKENS = 420 # facts carried up from one member's look
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def member_looks(look, members, fmt):
|
|
955
|
+
"""After the safety screen passed: the members that are documents, tables, media or binaries get their own look (written under the look folder,
|
|
956
|
+
never run), and their top facts are carried up here, because a list of names says nothing about what the file inside holds."""
|
|
957
|
+
if getattr(look, "depth", 0) >= 1 or look.quick or not hasattr(core, "recurse_into"):
|
|
958
|
+
return
|
|
959
|
+
read, close = member_reader(look, fmt)
|
|
960
|
+
if not read:
|
|
961
|
+
return
|
|
962
|
+
import dl_write
|
|
963
|
+
files = [m for m in members if m["kind"] == "file" and not m["enc"] and 0 < m["size"] <= MEMBER_LOOK_BYTES and os.path.splitext(m["name"])[1].lower() in MEMBER_LOOK_EXT]
|
|
964
|
+
t0, done = time.time(), 0
|
|
965
|
+
try:
|
|
966
|
+
for m in files[:MEMBER_LOOK_N]:
|
|
967
|
+
if time.time() - t0 > 150:
|
|
968
|
+
look.line(f"Member looks stopped after {done} members (time cap); the rest are named in the list.", 30)
|
|
969
|
+
break
|
|
970
|
+
try:
|
|
971
|
+
b = read(m["name"], MEMBER_LOOK_BYTES)
|
|
972
|
+
except Exception:
|
|
973
|
+
b = None
|
|
974
|
+
if b is None or len(b) > MEMBER_LOOK_BYTES:
|
|
975
|
+
continue
|
|
976
|
+
d = look.out / "members" / f"{done + 1:02d}"
|
|
977
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
978
|
+
fp = d / re.sub(r"[^\w.\-]+", "_", os.path.basename(m["name"].replace("\\", "/")))
|
|
979
|
+
fp.write_bytes(b)
|
|
980
|
+
tier = "quick" if os.path.splitext(fp.name)[1].lower() in (".mp3", ".wav", ".flac", ".ogg", ".opus", ".m4a", ".mp4", ".mkv", ".mov", ".avi", ".webm") and len(b) > (8 << 20) else "standard"
|
|
981
|
+
c = core.recurse_into(look, fp, m["name"], tier)
|
|
982
|
+
done += 1
|
|
983
|
+
chunks = [x for x in (c.stream or []) if x["sec"] in ("watch", "key")]
|
|
984
|
+
chunks.sort(key=lambda x: -x["pri"])
|
|
985
|
+
used, picked = 0, []
|
|
986
|
+
for x in chunks:
|
|
987
|
+
if used + x["tok"] > MEMBER_LOOK_TOKENS and picked:
|
|
988
|
+
break
|
|
989
|
+
picked.append(x["text"])
|
|
990
|
+
used += x["tok"]
|
|
991
|
+
if picked:
|
|
992
|
+
look.line(f"Member {m['name']} ({c.type}/{c.subtype}), looked at in place: " + " | ".join(" ".join(t.split()) for t in picked), 66, "member")
|
|
993
|
+
finally:
|
|
994
|
+
close()
|
|
995
|
+
|
|
996
|
+
|
|
821
997
|
def inner_tables(look, texts):
|
|
822
998
|
"""Each CSV/TSV/JSON/JSONL member: rows, columns and an exact field profile (sums, counts, extremes)."""
|
|
823
999
|
import dl_prof as P
|
|
@@ -876,14 +1052,14 @@ def parse_7z_slt(text):
|
|
|
876
1052
|
if in_files and cur.get("Path"):
|
|
877
1053
|
attr = cur.get("Attributes", "")
|
|
878
1054
|
kind = "dir" if cur.get("Folder") == "+" or attr.startswith("D") else "symlink" if cur.get("Symbolic Link") or attr.startswith("l") or attr.startswith("_l") else "file"
|
|
879
|
-
members.append(dict(name=cur["Path"], size=int(cur.get("Size") or 0), packed=int(cur.get("Packed Size") or 0), kind=kind, enc=cur.get("Encrypted") == "+", offset=
|
|
1055
|
+
members.append(dict(name=cur["Path"], size=int(cur.get("Size") or 0), packed=int(cur.get("Packed Size") or 0), kind=kind, enc=cur.get("Encrypted") == "+", offset=None))
|
|
880
1056
|
cur = {}
|
|
881
1057
|
continue
|
|
882
1058
|
if "=" in ln:
|
|
883
1059
|
k, v = ln.split("=", 1)
|
|
884
1060
|
(cur if in_files else extra).__setitem__(k.strip(), v.strip())
|
|
885
1061
|
if in_files and cur.get("Path"):
|
|
886
|
-
members.append(dict(name=cur["Path"], size=int(cur.get("Size") or 0), packed=int(cur.get("Packed Size") or 0), kind="file", enc=cur.get("Encrypted") == "+", offset=
|
|
1062
|
+
members.append(dict(name=cur["Path"], size=int(cur.get("Size") or 0), packed=int(cur.get("Packed Size") or 0), kind="file", enc=cur.get("Encrypted") == "+", offset=None))
|
|
887
1063
|
extra = {"method": extra.get("Method", ""), "solid": extra.get("Solid", ""), "type": extra.get("Type", "")}
|
|
888
1064
|
return members, extra
|
|
889
1065
|
|
|
@@ -1001,6 +1177,11 @@ def look_archive(look):
|
|
|
1001
1177
|
archive_contents(look, members, extra.get("format"))
|
|
1002
1178
|
except Exception as e: # the listing stands even when a member cannot be read
|
|
1003
1179
|
look.fail("member contents (manifests, tables, tree stats)", "stdlib", e)
|
|
1180
|
+
if not refusals and extra.get("format") in ("zip", "tar", "7z", "iso", "rar"):
|
|
1181
|
+
try:
|
|
1182
|
+
member_looks(look, members, extra.get("format"))
|
|
1183
|
+
except Exception as e:
|
|
1184
|
+
look.fail("member looks (documents and media inside the archive)", "stdlib", e)
|
|
1004
1185
|
rows =[(m["name"], m["size"], m["packed"], f"{m['size'] / max(1, m['packed']):.1f}" if m["packed"] else "", m["kind"], "encrypted" if m["enc"] else "") for m in members[:2000]]
|
|
1005
1186
|
buf = io.StringIO()
|
|
1006
1187
|
w = csv.writer(buf)
|
|
@@ -1227,9 +1408,15 @@ def look_font(look):
|
|
|
1227
1408
|
facts["family"] = names.get(1, "")
|
|
1228
1409
|
facts["subfamily"] = names.get(2, "")
|
|
1229
1410
|
facts["full_name"] = names.get(4, "")
|
|
1411
|
+
if font is not None:
|
|
1412
|
+
try:
|
|
1413
|
+
font_details(look, font, facts)
|
|
1414
|
+
except Exception as e:
|
|
1415
|
+
look.fail("font metrics", "fontTools", e)
|
|
1230
1416
|
for k, v in facts.items():
|
|
1231
1417
|
look.fact(k, v)
|
|
1232
1418
|
if facts.get("family") or facts.get("glyphs"):
|
|
1419
|
+
look.headline = f"Font {facts.get('full_name') or facts.get('family')!r} ({st}): {facts.get('glyphs', '?')} glyphs, {'variable' if facts.get('variable') else 'static'}; {len(facts.get('tables', []))} tables"
|
|
1233
1420
|
look.line(f"Font {facts.get('full_name') or facts.get('family')!r} ({st}): {facts.get('glyphs', '?')} glyphs, {'variable' if facts.get('variable') else 'static'}; "
|
|
1234
1421
|
f"{len(facts.get('tables', []))} tables.")
|
|
1235
1422
|
else:
|
|
@@ -1254,6 +1441,53 @@ def look_font(look):
|
|
|
1254
1441
|
look.ask_human("Which scripts or languages must this font cover?", "Is the licence in the file the one you expect?")
|
|
1255
1442
|
|
|
1256
1443
|
|
|
1444
|
+
def font_details(look, font, facts):
|
|
1445
|
+
"""Name records, design metrics, style bits and layout features: the facts a type designer or a web developer asks first."""
|
|
1446
|
+
h = font["head"]
|
|
1447
|
+
labels = {0: "copyright", 1: "family", 2: "subfamily", 3: "unique id", 4: "full name", 5: "version string", 6: "PostScript name", 7: "trademark", 8: "manufacturer",
|
|
1448
|
+
9: "designer", 11: "vendor URL", 12: "designer URL", 13: "licence", 14: "licence URL", 16: "typographic family", 17: "typographic subfamily"}
|
|
1449
|
+
got = {}
|
|
1450
|
+
for rec in font["name"].names:
|
|
1451
|
+
if rec.nameID in labels and rec.nameID not in got:
|
|
1452
|
+
try:
|
|
1453
|
+
got[rec.nameID] = rec.toUnicode()
|
|
1454
|
+
except Exception:
|
|
1455
|
+
pass
|
|
1456
|
+
look.line("Name records: " + "; ".join(f"{labels[i]} = {got[i][:90]}" for i in sorted(got)) + ".", 74)
|
|
1457
|
+
bits = []
|
|
1458
|
+
bits.append(f"{h.unitsPerEm} units per em, font revision {float(h.fontRevision):.3f}")
|
|
1459
|
+
bits.append(f"glyph box x {h.xMin}..{h.xMax}, y {h.yMin}..{h.yMax}")
|
|
1460
|
+
bits.append("macStyle: " + ", ".join(n for bit, n in ((1, "bold"), (2, "italic"), (4, "underline")) if h.macStyle & bit) if h.macStyle else "macStyle: regular")
|
|
1461
|
+
if "OS/2" in font:
|
|
1462
|
+
o = font["OS/2"]
|
|
1463
|
+
sel = [n for bit, n in ((1, "italic"), (32, "bold"), (64, "regular"), (512, "oblique")) if o.fsSelection & bit]
|
|
1464
|
+
bits.append(f"OS/2 weight class {o.usWeightClass}, width class {o.usWidthClass}, fsSelection {'+'.join(sel) or 'none'}, typo ascender {o.sTypoAscender}, typo descender {o.sTypoDescender}, "
|
|
1465
|
+
f"line gap {o.sTypoLineGap}, win ascent {o.usWinAscent}, win descent {o.usWinDescent}"
|
|
1466
|
+
+ (f", x-height {o.sxHeight}, cap height {o.sCapHeight}" if getattr(o, "version", 0) >= 2 and getattr(o, "sxHeight", 0) else ""))
|
|
1467
|
+
if "hhea" in font:
|
|
1468
|
+
hh = font["hhea"]
|
|
1469
|
+
bits.append(f"hhea ascent {hh.ascent}, descent {hh.descent}, line gap {hh.lineGap}")
|
|
1470
|
+
if "post" in font:
|
|
1471
|
+
bits.append(f"italic angle {font['post'].italicAngle}, fixed pitch {'yes' if font['post'].isFixedPitch else 'no'}")
|
|
1472
|
+
try:
|
|
1473
|
+
import datetime
|
|
1474
|
+
epoch = datetime.datetime(1904, 1, 1)
|
|
1475
|
+
bits.append(f"head created {(epoch + datetime.timedelta(seconds=h.created)).date()}, modified {(epoch + datetime.timedelta(seconds=h.modified)).date()}")
|
|
1476
|
+
except Exception:
|
|
1477
|
+
pass
|
|
1478
|
+
look.line("Design metrics: " + "; ".join(bits) + ".", 73)
|
|
1479
|
+
feats = {}
|
|
1480
|
+
for tab in ("GSUB", "GPOS"):
|
|
1481
|
+
if tab in font and getattr(font[tab].table, "FeatureList", None):
|
|
1482
|
+
feats[tab] = sorted({r.FeatureTag for r in font[tab].table.FeatureList.FeatureRecord})
|
|
1483
|
+
scripts = sorted({r.ScriptTag for tab in ("GSUB", "GPOS") if tab in font and getattr(font[tab].table, "ScriptList", None) for r in font[tab].table.ScriptList.ScriptRecord})
|
|
1484
|
+
if feats or scripts:
|
|
1485
|
+
look.line("Layout features: " + "; ".join(f"{t} {', '.join(v)}" for t, v in feats.items()) + (f"; scripts {', '.join(scripts)}" if scripts else "") + ".", 66)
|
|
1486
|
+
look.line(f"cmap subtables: {len(font['cmap'].tables)}; kern table {'present' if 'kern' in font else 'absent'}; hinting {'TrueType instructions (fpgm/prep)' if 'fpgm' in font or 'prep' in font else 'none or unhinted'}; "
|
|
1487
|
+
f"glyph outlines {'CFF (OpenType)' if 'CFF ' in font or 'CFF2' in font else 'TrueType quadratic'}.", 60)
|
|
1488
|
+
facts["units_per_em"] = h.unitsPerEm
|
|
1489
|
+
|
|
1490
|
+
|
|
1257
1491
|
def specimen(look, font, facts):
|
|
1258
1492
|
from PIL import Image, ImageDraw, ImageFont
|
|
1259
1493
|
buf = io.BytesIO()
|
|
@@ -43,7 +43,8 @@ def run_diec(look):
|
|
|
43
43
|
p = parse_diec(out)
|
|
44
44
|
packers = [e for e in p["entries"] if e[0].lower() in ("packer", "protector", "cryptor")]
|
|
45
45
|
keep = [(c, n, v) for c, n, v in p["entries"] if c.lower() in ("linker", "compiler", "tool", "packer", "protector", "installer", "library")][:4]
|
|
46
|
-
|
|
46
|
+
if keep:
|
|
47
|
+
look.line("Detect It Easy: " + "; ".join(f"{c.lower()} {n}" + (f" {v}" if v else "") for c, n, v in keep) + ".", 68)
|
|
47
48
|
if packers:
|
|
48
49
|
look.flag("PACKED?", "Detect It Easy", ", ".join(f"{n} {v}".strip() for _, n, v in packers), "a packer or protector is identified: static strings and imports describe the stub, not the program")
|
|
49
50
|
look.step("packer and compiler identification", "diec")
|