richprint-pe 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
richprint/__init__.py ADDED
@@ -0,0 +1,62 @@
1
+ """
2
+ richprint - Decode and print Rich headers from Windows PE executables.
3
+
4
+ The Rich header is metadata embedded by Microsoft's linker containing
5
+ compiler version information (@comp.id records).
6
+ """
7
+
8
+ from .parser import parse_file, parse_bytes
9
+ from .database import load_database, lookup_description, CompilerDatabase
10
+ from .models import CompilerEntry, RichHeader, PEInfo, ParseResult
11
+ from .constants import (
12
+ MZ_SIGNATURE,
13
+ PE_SIGNATURE,
14
+ RICH_SIGNATURE,
15
+ DANS_SIGNATURE,
16
+ MACHINE_TYPES,
17
+ get_machine_type,
18
+ )
19
+ from .exceptions import (
20
+ RichPrintError,
21
+ FileOpenError,
22
+ NoMZHeaderError,
23
+ NoPEHeaderError,
24
+ InvalidDOSHeaderError,
25
+ NoRichHeaderError,
26
+ NoDanSTokenError,
27
+ InvalidRichHeaderError,
28
+ )
29
+
30
+ __version__ = "1.0.0"
31
+
32
+ __all__ = [
33
+ # Main API
34
+ "parse_file",
35
+ "parse_bytes",
36
+ "load_database",
37
+ "lookup_description",
38
+ # Models
39
+ "CompilerEntry",
40
+ "RichHeader",
41
+ "PEInfo",
42
+ "ParseResult",
43
+ "CompilerDatabase",
44
+ # Constants
45
+ "MZ_SIGNATURE",
46
+ "PE_SIGNATURE",
47
+ "RICH_SIGNATURE",
48
+ "DANS_SIGNATURE",
49
+ "MACHINE_TYPES",
50
+ "get_machine_type",
51
+ # Exceptions
52
+ "RichPrintError",
53
+ "FileOpenError",
54
+ "NoMZHeaderError",
55
+ "NoPEHeaderError",
56
+ "InvalidDOSHeaderError",
57
+ "NoRichHeaderError",
58
+ "NoDanSTokenError",
59
+ "InvalidRichHeaderError",
60
+ # Version
61
+ "__version__",
62
+ ]
richprint/__main__.py ADDED
@@ -0,0 +1,7 @@
1
+ """Enable running as: python -m richprint"""
2
+
3
+ import sys
4
+ from .cli import main
5
+
6
+ if __name__ == "__main__":
7
+ sys.exit(main())
richprint/cli.py ADDED
@@ -0,0 +1,112 @@
1
+ """Command-line interface for richprint."""
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ from typing import List, Optional
7
+
8
+ from .database import load_database
9
+ from .parser import parse_file
10
+ from .models import ParseResult
11
+
12
+
13
+ def format_entry_line(entry) -> str:
14
+ """Format a single Rich header entry for display."""
15
+ return (
16
+ f"{entry.comp_id:08x} {entry.product_id:4x} {entry.build_version:6d} "
17
+ f"{entry.count:5d}"
18
+ + (f" {entry.description}" if entry.description else "")
19
+ )
20
+
21
+
22
+ def print_result(result: ParseResult) -> None:
23
+ """Print parse result in human-readable format."""
24
+ print(f"Processing {result.filename}")
25
+
26
+ if not result.success:
27
+ print(result.error, file=sys.stderr)
28
+ return
29
+
30
+ if result.pe_info:
31
+ print(f"Target machine: {result.pe_info.machine_name}")
32
+
33
+ if result.rich_header and result.rich_header.entries:
34
+ print("@comp.id id version count description")
35
+ for entry in result.rich_header.entries:
36
+ print(format_entry_line(entry))
37
+
38
+
39
+ def create_parser() -> argparse.ArgumentParser:
40
+ """Create argument parser."""
41
+ parser = argparse.ArgumentParser(
42
+ prog="richprint",
43
+ description="Decode and print Rich headers from Windows PE executables.",
44
+ epilog=(
45
+ "Rich headers contain compiler version information embedded by "
46
+ "Microsoft's linker."
47
+ ),
48
+ )
49
+
50
+ parser.add_argument(
51
+ "files",
52
+ nargs="*",
53
+ metavar="FILE",
54
+ help="PE executable file(s) to analyze",
55
+ )
56
+
57
+ parser.add_argument(
58
+ "--json",
59
+ action="store_true",
60
+ help="Output results as JSON",
61
+ )
62
+
63
+ parser.add_argument(
64
+ "--database", "-d",
65
+ metavar="PATH",
66
+ help="Path to custom comp_id.txt database file",
67
+ )
68
+
69
+ parser.add_argument(
70
+ "--version", "-V",
71
+ action="version",
72
+ version="%(prog)s 1.0.0",
73
+ )
74
+
75
+ return parser
76
+
77
+
78
+ def main(argv: Optional[List[str]] = None) -> int:
79
+ """Main entry point for CLI."""
80
+ parser = create_parser()
81
+ args = parser.parse_args(argv)
82
+
83
+ if not args.files:
84
+ print(
85
+ "Rich header decoder. Usage:\n\n"
86
+ " richprint file ...\n\n"
87
+ "Rich headers can be found in executable files, DLLs, "
88
+ "and other binary files\ncreated by Microsoft linker."
89
+ )
90
+ return 0
91
+
92
+ # Load database
93
+ db = load_database(args.database)
94
+
95
+ results = []
96
+ for filename in args.files:
97
+ result = parse_file(filename, db)
98
+ results.append(result)
99
+
100
+ if args.json:
101
+ output = [r.to_dict() for r in results]
102
+ print(json.dumps(output, indent=2))
103
+ else:
104
+ for result in results:
105
+ print_result(result)
106
+
107
+ # Return non-zero if any file failed
108
+ return 0 if all(r.success for r in results) else 1
109
+
110
+
111
+ if __name__ == "__main__":
112
+ sys.exit(main())
richprint/constants.py ADDED
@@ -0,0 +1,47 @@
1
+ """Magic numbers and constants for PE/Rich header parsing."""
2
+
3
+ # Signature magic values
4
+ MZ_SIGNATURE = 0x5A4D # "MZ" - DOS executable signature
5
+ PE_SIGNATURE = 0x4550 # "PE\0\0" - PE header signature
6
+ RICH_SIGNATURE = 0x68636952 # "Rich" (little-endian)
7
+ DANS_SIGNATURE = 0x536E6144 # "DanS" (little-endian)
8
+
9
+ # DOS header offsets
10
+ DOS_NUM_RELOCS_OFFSET = 0x06 # Number of relocations
11
+ DOS_HEADER_PARA_OFFSET = 0x08 # Size of header in paragraphs
12
+ DOS_RELOC_OFFSET = 0x18 # File address of relocation table
13
+ DOS_PE_OFFSET = 0x3C # File address of PE header
14
+
15
+ # PE header offsets (relative to PE signature)
16
+ PE_MACHINE_OFFSET = 4 # Machine type field
17
+
18
+ # Machine type mapping
19
+ # From https://msdn.microsoft.com/en-us/windows/hardware/gg463119.aspx
20
+ MACHINE_TYPES = {
21
+ 0x8664: "x64",
22
+ 0x14C: "x32",
23
+ 0x1D3: "Matsushita AM33",
24
+ 0x1C0: "ARM LE",
25
+ 0x1C4: "ARMv7+ Thumb",
26
+ 0xAA64: "ARMv8 64bit",
27
+ 0xEBC: "EFI bytecode",
28
+ 0x200: "Intel Itanium",
29
+ 0x9041: "Mitsubishi M32R LE",
30
+ 0x266: "MIPS16",
31
+ 0x366: "MIPS w/FPU",
32
+ 0x466: "MIPS16 w/FPU",
33
+ 0x1F0: "PowerPC LE",
34
+ 0x1F1: "PowerPC w/FPU",
35
+ 0x166: "MIPS LE",
36
+ 0x1A2: "Hitachi SH3",
37
+ 0x1A3: "Hitachi SH3 DSP",
38
+ 0x1A6: "Hitachi SH4",
39
+ 0x1A8: "Hitachi SH5",
40
+ 0x1C2: "ARM or Thumb",
41
+ 0x169: "MIPS LE WCE v2",
42
+ }
43
+
44
+
45
+ def get_machine_type(machine_id: int) -> str:
46
+ """Get human-readable machine type name."""
47
+ return MACHINE_TYPES.get(machine_id, "Unknown")
@@ -0,0 +1 @@
1
+ # Package marker for bundled data files.