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/database.py ADDED
@@ -0,0 +1,94 @@
1
+ """Compiler ID database loading and lookup."""
2
+
3
+ import importlib.resources
4
+ from typing import Dict, Optional
5
+
6
+ # Type alias for the database
7
+ CompilerDatabase = Dict[int, str]
8
+
9
+
10
+ def load_database(path: Optional[str] = None) -> CompilerDatabase:
11
+ """
12
+ Load compiler ID database from file.
13
+
14
+ Args:
15
+ path: Path to comp_id.txt file. If None, uses bundled database.
16
+
17
+ Returns:
18
+ Dictionary mapping comp.id values to descriptions.
19
+ """
20
+ descriptions: CompilerDatabase = {}
21
+
22
+ if path is None:
23
+ # Use bundled database
24
+ try:
25
+ # Python 3.9+
26
+ files = importlib.resources.files("richprint.data")
27
+ content = (files / "comp_id.txt").read_text(encoding="utf-8")
28
+ except AttributeError:
29
+ # Python 3.8 fallback
30
+ import pkg_resources
31
+ content = pkg_resources.resource_string(
32
+ "richprint.data", "comp_id.txt"
33
+ ).decode("utf-8")
34
+ lines = content.splitlines()
35
+ else:
36
+ try:
37
+ with open(path, "r", encoding="utf-8") as f:
38
+ lines = f.readlines()
39
+ except (IOError, OSError):
40
+ return descriptions
41
+
42
+ for line in lines:
43
+ line = line.rstrip("\n\r")
44
+
45
+ # Remove trailing comments
46
+ comment_pos = line.rfind("#")
47
+ if comment_pos != -1:
48
+ # Trim trailing spaces before comment
49
+ while comment_pos > 0 and line[comment_pos - 1] == " ":
50
+ comment_pos -= 1
51
+ line = line[:comment_pos]
52
+
53
+ # Skip empty lines and comment-only lines
54
+ if len(line) <= 8 or line.startswith("#"):
55
+ continue
56
+
57
+ # Parse: <hex_id> <description>
58
+ try:
59
+ hex_part = line[:8]
60
+ comp_id = int(hex_part, 16)
61
+ desc = line[9:] if len(line) > 9 else ""
62
+
63
+ # Skip duplicates (keep first)
64
+ if comp_id not in descriptions:
65
+ descriptions[comp_id] = desc
66
+ except ValueError:
67
+ continue
68
+
69
+ return descriptions
70
+
71
+
72
+ def lookup_description(
73
+ db: CompilerDatabase, comp_id: int, product_id: int
74
+ ) -> str:
75
+ """
76
+ Look up description for a compiler entry.
77
+
78
+ First tries exact comp_id match, then falls back to product_id only.
79
+
80
+ Args:
81
+ db: Compiler database dictionary.
82
+ comp_id: Full compiler ID (product_id << 16 | build_version).
83
+ product_id: Product ID (high 16 bits of comp_id).
84
+
85
+ Returns:
86
+ Description string, or empty string if not found.
87
+ """
88
+ # Try exact match first
89
+ if comp_id in db:
90
+ return db[comp_id]
91
+ # Fall back to product_id only
92
+ if product_id in db:
93
+ return db[product_id]
94
+ return ""
@@ -0,0 +1,41 @@
1
+ """Custom exceptions for richprint."""
2
+
3
+
4
+ class RichPrintError(Exception):
5
+ """Base exception for richprint errors."""
6
+ pass
7
+
8
+
9
+ class FileOpenError(RichPrintError):
10
+ """Failed to open or read file."""
11
+ pass
12
+
13
+
14
+ class NoMZHeaderError(RichPrintError):
15
+ """File does not have MZ (DOS) header signature."""
16
+ pass
17
+
18
+
19
+ class NoPEHeaderError(RichPrintError):
20
+ """File does not have valid PE header."""
21
+ pass
22
+
23
+
24
+ class InvalidDOSHeaderError(RichPrintError):
25
+ """DOS header has invalid values."""
26
+ pass
27
+
28
+
29
+ class NoRichHeaderError(RichPrintError):
30
+ """Rich header not found in file."""
31
+ pass
32
+
33
+
34
+ class NoDanSTokenError(RichPrintError):
35
+ """Rich header's DanS token not found."""
36
+ pass
37
+
38
+
39
+ class InvalidRichHeaderError(RichPrintError):
40
+ """Rich header structure is invalid."""
41
+ pass
richprint/models.py ADDED
@@ -0,0 +1,72 @@
1
+ """Data models for richprint."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import List, Optional
5
+
6
+
7
+ @dataclass
8
+ class CompilerEntry:
9
+ """A single entry from the Rich header."""
10
+ comp_id: int # Full @comp.id value (product_id << 16 | build_version)
11
+ product_id: int # Product/tool identifier (high 16 bits)
12
+ build_version: int # Build version number (low 16 bits)
13
+ count: int # Number of objects with this comp.id
14
+ description: str = "" # Human-readable description from database
15
+
16
+
17
+ @dataclass
18
+ class RichHeader:
19
+ """Parsed Rich header data."""
20
+ xor_key: int # XOR key used to encode the header
21
+ entries: List[CompilerEntry] = field(default_factory=list)
22
+ dans_offset: int = 0 # File offset of DanS marker
23
+ rich_offset: int = 0 # File offset of Rich marker
24
+
25
+
26
+ @dataclass
27
+ class PEInfo:
28
+ """Basic PE file information."""
29
+ machine_type: int # Machine type value
30
+ machine_name: str # Human-readable machine name
31
+ pe_offset: int # File offset of PE header
32
+ dos_stub_end: int # End of DOS stub (start of search area)
33
+
34
+
35
+ @dataclass
36
+ class ParseResult:
37
+ """Complete result of parsing a PE file."""
38
+ filename: str
39
+ success: bool = False
40
+ error: Optional[str] = None
41
+ pe_info: Optional[PEInfo] = None
42
+ rich_header: Optional[RichHeader] = None
43
+
44
+ def to_dict(self) -> dict:
45
+ """Convert result to dictionary for JSON serialization."""
46
+ result = {
47
+ "filename": self.filename,
48
+ "success": self.success,
49
+ }
50
+ if self.error:
51
+ result["error"] = self.error
52
+ if self.pe_info:
53
+ result["pe_info"] = {
54
+ "machine_type": self.pe_info.machine_type,
55
+ "machine_name": self.pe_info.machine_name,
56
+ "pe_offset": self.pe_info.pe_offset,
57
+ }
58
+ if self.rich_header:
59
+ result["rich_header"] = {
60
+ "xor_key": f"0x{self.rich_header.xor_key:08x}",
61
+ "entries": [
62
+ {
63
+ "comp_id": f"0x{e.comp_id:08x}",
64
+ "product_id": e.product_id,
65
+ "build_version": e.build_version,
66
+ "count": e.count,
67
+ "description": e.description,
68
+ }
69
+ for e in self.rich_header.entries
70
+ ],
71
+ }
72
+ return result
richprint/parser.py ADDED
@@ -0,0 +1,338 @@
1
+ """Core PE/Rich header parsing logic."""
2
+
3
+ import struct
4
+ from typing import BinaryIO, List, Optional, Tuple
5
+
6
+ from .constants import (
7
+ MZ_SIGNATURE,
8
+ PE_SIGNATURE,
9
+ RICH_SIGNATURE,
10
+ DANS_SIGNATURE,
11
+ DOS_NUM_RELOCS_OFFSET,
12
+ DOS_HEADER_PARA_OFFSET,
13
+ DOS_RELOC_OFFSET,
14
+ DOS_PE_OFFSET,
15
+ PE_MACHINE_OFFSET,
16
+ get_machine_type,
17
+ )
18
+ from .database import CompilerDatabase, lookup_description
19
+ from .exceptions import (
20
+ FileOpenError,
21
+ NoMZHeaderError,
22
+ NoPEHeaderError,
23
+ InvalidDOSHeaderError,
24
+ NoRichHeaderError,
25
+ NoDanSTokenError,
26
+ InvalidRichHeaderError,
27
+ )
28
+ from .models import CompilerEntry, RichHeader, PEInfo, ParseResult
29
+
30
+
31
+ def read_word(data: bytes, offset: int) -> int:
32
+ """Read unsigned 16-bit little-endian value."""
33
+ return struct.unpack_from("<H", data, offset)[0]
34
+
35
+
36
+ def read_dword(data: bytes, offset: int) -> int:
37
+ """Read unsigned 32-bit little-endian value."""
38
+ return struct.unpack_from("<I", data, offset)[0]
39
+
40
+
41
+ def parse_pe_info(data: bytes) -> PEInfo:
42
+ """
43
+ Parse basic PE information from file data.
44
+
45
+ Args:
46
+ data: File contents as bytes.
47
+
48
+ Returns:
49
+ PEInfo with machine type, PE offset, and DOS stub end offset.
50
+
51
+ Raises:
52
+ NoMZHeaderError: If MZ signature not found.
53
+ InvalidDOSHeaderError: If DOS header values are invalid.
54
+ NoPEHeaderError: If PE signature not found.
55
+ """
56
+ # Check MZ header
57
+ if len(data) < 2:
58
+ raise NoMZHeaderError("File too small for MZ header")
59
+
60
+ mz = read_word(data, 0)
61
+ if mz != MZ_SIGNATURE:
62
+ raise NoMZHeaderError(f"No MZ header - magic is: 0x{mz:x}")
63
+
64
+ # Read DOS header metrics
65
+ if len(data) < 0x40:
66
+ raise InvalidDOSHeaderError("File too small for DOS header")
67
+
68
+ num_relocs = read_word(data, DOS_NUM_RELOCS_OFFSET)
69
+ header_para = read_word(data, DOS_HEADER_PARA_OFFSET)
70
+
71
+ if header_para < 4:
72
+ raise InvalidDOSHeaderError(
73
+ f"Too few paragraphs in DOS header: {header_para}, not a PE executable"
74
+ )
75
+
76
+ reloc_offset = read_word(data, DOS_RELOC_OFFSET)
77
+ pe_offset = read_word(data, DOS_PE_OFFSET)
78
+
79
+ if pe_offset < header_para * 16:
80
+ raise InvalidDOSHeaderError(
81
+ f"PE offset is too small: {pe_offset}, not a PE executable"
82
+ )
83
+
84
+ # Check PE signature
85
+ if len(data) < pe_offset + 6:
86
+ raise NoPEHeaderError("File too small for PE header")
87
+
88
+ pe_sig = read_dword(data, pe_offset)
89
+ if pe_sig != PE_SIGNATURE:
90
+ raise NoPEHeaderError(
91
+ f"No PE header signature: 0x{pe_sig:x}, not a PE executable"
92
+ )
93
+
94
+ # Get machine type
95
+ machine_type = read_word(data, pe_offset + PE_MACHINE_OFFSET)
96
+ machine_name = get_machine_type(machine_type)
97
+
98
+ # Calculate DOS stub end offset
99
+ dos_stub_end = reloc_offset
100
+ if num_relocs > 0:
101
+ dos_stub_end += 4 * num_relocs
102
+
103
+ # Align to 16-byte paragraph boundary
104
+ if dos_stub_end % 16:
105
+ dos_stub_end += 16 - (dos_stub_end % 16)
106
+
107
+ return PEInfo(
108
+ machine_type=machine_type,
109
+ machine_name=machine_name,
110
+ pe_offset=pe_offset,
111
+ dos_stub_end=dos_stub_end,
112
+ )
113
+
114
+
115
+ def find_rich_header(data: bytes, pe_info: PEInfo) -> Tuple[int, int, int]:
116
+ """
117
+ Find Rich header markers and XOR key.
118
+
119
+ Args:
120
+ data: File contents as bytes.
121
+ pe_info: Parsed PE info with search boundaries.
122
+
123
+ Returns:
124
+ Tuple of (rich_offset, dans_offset, xor_key).
125
+
126
+ Raises:
127
+ NoRichHeaderError: If Rich signature not found.
128
+ NoDanSTokenError: If DanS token not found.
129
+ InvalidRichHeaderError: If header structure is invalid.
130
+ """
131
+ start = pe_info.dos_stub_end
132
+ end = pe_info.pe_offset
133
+
134
+ # Search for "Rich" signature
135
+ rich_offset = -1
136
+ for i in range(start, end, 4):
137
+ if i + 4 > len(data):
138
+ break
139
+ val = read_dword(data, i)
140
+ if val == RICH_SIGNATURE:
141
+ rich_offset = i
142
+ break
143
+
144
+ if rich_offset == -1:
145
+ raise NoRichHeaderError("Rich header not found")
146
+
147
+ # XOR key is immediately after "Rich"
148
+ if rich_offset + 8 > len(data):
149
+ raise InvalidRichHeaderError("File truncated after Rich signature")
150
+
151
+ xor_key = read_dword(data, rich_offset + 4)
152
+
153
+ # Search for "DanS" signature (XOR'd with key)
154
+ dans_offset = -1
155
+ target = DANS_SIGNATURE ^ xor_key
156
+ for i in range(start, end, 4):
157
+ if i + 4 > len(data):
158
+ break
159
+ val = read_dword(data, i)
160
+ if val == target:
161
+ dans_offset = i
162
+ break
163
+
164
+ if dans_offset == -1:
165
+ raise NoDanSTokenError("Rich header's DanS token not found")
166
+
167
+ # Validate end offset doesn't run into PE header
168
+ end_offset = rich_offset + 8 # Rich + key
169
+ if end_offset > pe_info.pe_offset:
170
+ raise InvalidRichHeaderError(
171
+ f"Calculated end offset runs into PE header: 0x{end_offset:x}"
172
+ )
173
+
174
+ return rich_offset, dans_offset, xor_key
175
+
176
+
177
+ def decode_rich_header(
178
+ data: bytes,
179
+ rich_offset: int,
180
+ dans_offset: int,
181
+ xor_key: int,
182
+ db: Optional[CompilerDatabase] = None,
183
+ ) -> RichHeader:
184
+ """
185
+ Decode Rich header entries.
186
+
187
+ Args:
188
+ data: File contents as bytes.
189
+ rich_offset: File offset of Rich marker.
190
+ dans_offset: File offset of DanS marker.
191
+ xor_key: XOR key for decoding.
192
+ db: Optional compiler database for descriptions.
193
+
194
+ Returns:
195
+ RichHeader with decoded entries.
196
+ """
197
+ entries: List[CompilerEntry] = []
198
+
199
+ # Entries start at DanS + 16 (skip DanS + 3 padding DWORDs)
200
+ # Entries end at Rich - 8 (stop before last empty entry)
201
+ start = dans_offset + 16
202
+ end = rich_offset
203
+
204
+ for pos in range(start, end, 8):
205
+ if pos + 8 > len(data):
206
+ break
207
+
208
+ # Read and decode version and count
209
+ ver_raw = read_dword(data, pos)
210
+ count_raw = read_dword(data, pos + 4)
211
+
212
+ ver = ver_raw ^ xor_key
213
+ count = count_raw ^ xor_key
214
+
215
+ # Extract product_id and build_version
216
+ product_id = ver >> 16
217
+ build_version = ver & 0xFFFF
218
+
219
+ # Look up description
220
+ description = ""
221
+ if db is not None:
222
+ description = lookup_description(db, ver, product_id)
223
+
224
+ entries.append(CompilerEntry(
225
+ comp_id=ver,
226
+ product_id=product_id,
227
+ build_version=build_version,
228
+ count=count,
229
+ description=description,
230
+ ))
231
+
232
+ return RichHeader(
233
+ xor_key=xor_key,
234
+ entries=entries,
235
+ dans_offset=dans_offset,
236
+ rich_offset=rich_offset,
237
+ )
238
+
239
+
240
+ def parse_file(
241
+ filename: str,
242
+ db: Optional[CompilerDatabase] = None,
243
+ ) -> ParseResult:
244
+ """
245
+ Parse Rich header from a PE file.
246
+
247
+ Args:
248
+ filename: Path to PE file.
249
+ db: Optional compiler database for descriptions.
250
+
251
+ Returns:
252
+ ParseResult with parsed data or error information.
253
+ """
254
+ result = ParseResult(filename=filename)
255
+
256
+ try:
257
+ with open(filename, "rb") as f:
258
+ data = f.read()
259
+ except (IOError, OSError) as e:
260
+ result.error = f"Failed to open file: {e}"
261
+ return result
262
+
263
+ try:
264
+ # Parse PE info
265
+ pe_info = parse_pe_info(data)
266
+ result.pe_info = pe_info
267
+
268
+ # Find Rich header
269
+ rich_offset, dans_offset, xor_key = find_rich_header(data, pe_info)
270
+
271
+ # Decode entries
272
+ rich_header = decode_rich_header(
273
+ data, rich_offset, dans_offset, xor_key, db
274
+ )
275
+ result.rich_header = rich_header
276
+ result.success = True
277
+
278
+ except (
279
+ NoMZHeaderError,
280
+ NoPEHeaderError,
281
+ InvalidDOSHeaderError,
282
+ NoRichHeaderError,
283
+ NoDanSTokenError,
284
+ InvalidRichHeaderError,
285
+ ) as e:
286
+ result.error = str(e)
287
+ except Exception as e:
288
+ result.error = f"Unexpected error: {e}"
289
+
290
+ return result
291
+
292
+
293
+ def parse_bytes(
294
+ data: bytes,
295
+ db: Optional[CompilerDatabase] = None,
296
+ filename: str = "<bytes>",
297
+ ) -> ParseResult:
298
+ """
299
+ Parse Rich header from raw bytes.
300
+
301
+ Args:
302
+ data: PE file contents as bytes.
303
+ db: Optional compiler database for descriptions.
304
+ filename: Optional filename for result.
305
+
306
+ Returns:
307
+ ParseResult with parsed data or error information.
308
+ """
309
+ result = ParseResult(filename=filename)
310
+
311
+ try:
312
+ # Parse PE info
313
+ pe_info = parse_pe_info(data)
314
+ result.pe_info = pe_info
315
+
316
+ # Find Rich header
317
+ rich_offset, dans_offset, xor_key = find_rich_header(data, pe_info)
318
+
319
+ # Decode entries
320
+ rich_header = decode_rich_header(
321
+ data, rich_offset, dans_offset, xor_key, db
322
+ )
323
+ result.rich_header = rich_header
324
+ result.success = True
325
+
326
+ except (
327
+ NoMZHeaderError,
328
+ NoPEHeaderError,
329
+ InvalidDOSHeaderError,
330
+ NoRichHeaderError,
331
+ NoDanSTokenError,
332
+ InvalidRichHeaderError,
333
+ ) as e:
334
+ result.error = str(e)
335
+ except Exception as e:
336
+ result.error = f"Unexpected error: {e}"
337
+
338
+ return result
@@ -0,0 +1,119 @@
1
+ Metadata-Version: 2.4
2
+ Name: richprint-pe
3
+ Version: 1.0.0
4
+ Summary: Decode and print Rich headers from Windows PE executables
5
+ Project-URL: Homepage, https://github.com/dishather/richprint
6
+ Project-URL: Repository, https://github.com/dishather/richprint
7
+ Author-email: dishather <noreply@github.com>
8
+ License-Expression: BSD-2-Clause
9
+ License-File: LICENSE
10
+ Keywords: compiler,executable,forensics,pe,rich-header,windows
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: BSD License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Software Development :: Build Tools
23
+ Classifier: Topic :: System :: Systems Administration
24
+ Requires-Python: >=3.8
25
+ Description-Content-Type: text/markdown
26
+
27
+ # richprint
28
+
29
+ A Python tool to decode and print compiler information stored in the Rich Header of Windows PE executables.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install richprint-pe
35
+ ```
36
+
37
+ ## What is the Rich Header?
38
+
39
+ The Rich Header is a section of binary data created by Microsoft's linker, located between the DOS stub and PE header in Windows executables. It contains a list of compiler/tool IDs (@comp.id) used to build the executable, allowing identification of exact compiler versions down to build numbers.
40
+
41
+ The data is XOR-encoded, with "Rich" being the only readable marker. Files created by non-Microsoft linkers will not have this header.
42
+
43
+ For technical details, see [Daniel Pistelli's article](http://www.ntcore.com/files/richsign.htm).
44
+
45
+ ## Usage
46
+
47
+ ### Command Line
48
+
49
+ ```bash
50
+ # Analyze one or more files
51
+ richprint notepad.exe
52
+ richprint file1.exe file2.dll file3.sys
53
+
54
+ # JSON output
55
+ richprint --json notepad.exe
56
+
57
+ # Use custom compiler ID database
58
+ richprint --database /path/to/comp_id.txt notepad.exe
59
+ ```
60
+
61
+ ### Python API
62
+
63
+ ```python
64
+ from richprint import parse_file, load_database
65
+
66
+ # Load the bundled compiler ID database
67
+ db = load_database()
68
+
69
+ # Parse a PE file
70
+ result = parse_file("notepad.exe", db)
71
+
72
+ if result.success:
73
+ print(f"Machine: {result.pe_info.machine_name}")
74
+ print(f"XOR Key: 0x{result.rich_header.xor_key:08x}")
75
+ for entry in result.rich_header.entries:
76
+ print(f" {entry.comp_id:08x} {entry.description}")
77
+ else:
78
+ print(f"Error: {result.error}")
79
+ ```
80
+
81
+ ## Output Format
82
+
83
+ ```
84
+ Processing notepad.exe
85
+ Target machine: x64
86
+ @comp.id id version count description
87
+ 00e1520d e1 21005 10 [C++] VS2013 build 21005
88
+ 00df520d df 21005 1 [ASM] VS2013 build 21005
89
+ 00de520d de 21005 1 [LNK] VS2013 build 21005
90
+ ```
91
+
92
+ ## Compiler ID Database
93
+
94
+ The bundled `comp_id.txt` database maps compiler IDs to human-readable descriptions. The format supports:
95
+
96
+ - `[ C ]` - C compiler
97
+ - `[C++]` - C++ compiler
98
+ - `[ASM]` - Assembler
99
+ - `[LNK]` - Linker
100
+ - `[RES]` - Resource converter
101
+ - `[IMP]` / `[EXP]` - DLL import/export records
102
+ - And many more...
103
+
104
+ ## Suppressing Rich Headers
105
+
106
+ To prevent Microsoft tools from emitting this header, use the undocumented linker option:
107
+ ```
108
+ /emittoolversioninfo:no
109
+ ```
110
+
111
+ Available since VS2019 Update 11.
112
+
113
+ ## License
114
+
115
+ BSD 2-Clause License. See [LICENSE](LICENSE) for details.
116
+
117
+ ## Credits
118
+
119
+ Original C++ implementation and compiler ID database by [dishather](https://github.com/dishather/richprint).
@@ -0,0 +1,15 @@
1
+ richprint/__init__.py,sha256=9Z_Z13IE3ib5zzuoeiGDJQ5PMxOfhHlwIejZWeTHWGc,1438
2
+ richprint/__main__.py,sha256=9u51HKotzF8v0FOOjE8xKDe-RgqpLbfVHa19yxZOado,135
3
+ richprint/cli.py,sha256=6nbp8AxonlXZRwPHp_8DEVccASuZ6OSlleGyzAfAuLQ,3017
4
+ richprint/constants.py,sha256=ueg9NjzlRaeymExJTMwa5DJRPyzDulDRUDvsyqt0NrI,1502
5
+ richprint/database.py,sha256=QrW1v84wZaOdJqbK-BSEjH46-1dLkLXHi5fKzbWhqL8,2793
6
+ richprint/exceptions.py,sha256=2_wSOdPszMhmE9syhkw42HlaBfcpGJBgabaiVH0ocGs,848
7
+ richprint/models.py,sha256=agKEUybeg62NMvv-haOel86hCumowPBfCZSK3p122aI,2475
8
+ richprint/parser.py,sha256=RCv-L4u247out-9d1Chv8vfIQZDDAgaXq3zM3dhA9gk,9393
9
+ richprint/data/__init__.py,sha256=rkFDdGUU9barWpkoJjOqqk7VnOQVHgowqMPy-W7FNVE,42
10
+ richprint/data/comp_id.txt,sha256=GGyxqv0yR5ek0UhZ1Wb2jvlh-2f6USD-yi1RNxxf82A,205937
11
+ richprint_pe-1.0.0.dist-info/METADATA,sha256=EIxEVkJPQVCVkQPsC56186c67VZrUXO60Bso5kyCRXc,3574
12
+ richprint_pe-1.0.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
13
+ richprint_pe-1.0.0.dist-info/entry_points.txt,sha256=GaBZUbl51q-6xLefwvWwy5NzRjV2In9p7-WQPPEScW4,49
14
+ richprint_pe-1.0.0.dist-info/licenses/LICENSE,sha256=ej8fVsICMQH4n8rjr8BBLevIsip0bVoyLGID5Jx80SA,1333
15
+ richprint_pe-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ richprint = richprint.cli:main