hacktools 0.38.7__tar.gz → 0.40.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 (33) hide show
  1. {hacktools-0.38.7/hacktools.egg-info → hacktools-0.40.0}/PKG-INFO +4 -3
  2. hacktools-0.40.0/hacktools/__init__.py +1 -0
  3. {hacktools-0.38.7 → hacktools-0.40.0}/hacktools/c_ext/cmp_lzss.c +8 -2
  4. {hacktools-0.38.7 → hacktools-0.40.0}/hacktools/common.py +1 -1
  5. {hacktools-0.38.7 → hacktools-0.40.0}/hacktools/nds.py +2 -2
  6. {hacktools-0.38.7 → hacktools-0.40.0}/hacktools/psp.py +162 -0
  7. {hacktools-0.38.7 → hacktools-0.40.0}/hacktools/psx.py +5 -5
  8. {hacktools-0.38.7 → hacktools-0.40.0/hacktools.egg-info}/PKG-INFO +4 -3
  9. {hacktools-0.38.7 → hacktools-0.40.0}/setup.py +2 -2
  10. hacktools-0.38.7/hacktools/__init__.py +0 -1
  11. {hacktools-0.38.7 → hacktools-0.40.0}/LICENSE +0 -0
  12. {hacktools-0.38.7 → hacktools-0.40.0}/MANIFEST.in +0 -0
  13. {hacktools-0.38.7 → hacktools-0.40.0}/README.md +0 -0
  14. {hacktools-0.38.7 → hacktools-0.40.0}/hacktools/arch.py +0 -0
  15. {hacktools-0.38.7 → hacktools-0.40.0}/hacktools/assets/icon.png +0 -0
  16. {hacktools-0.38.7 → hacktools-0.40.0}/hacktools/c_ext/cmp_cri.c +0 -0
  17. {hacktools-0.38.7 → hacktools-0.40.0}/hacktools/c_ext/cmp_misc.c +0 -0
  18. {hacktools-0.38.7 → hacktools-0.40.0}/hacktools/c_ext/cmp_racjin.c +0 -0
  19. {hacktools-0.38.7 → hacktools-0.40.0}/hacktools/c_ext/inc.h +0 -0
  20. {hacktools-0.38.7 → hacktools-0.40.0}/hacktools/compression.py +0 -0
  21. {hacktools-0.38.7 → hacktools-0.40.0}/hacktools/cpk.py +0 -0
  22. {hacktools-0.38.7 → hacktools-0.40.0}/hacktools/gb.py +0 -0
  23. {hacktools-0.38.7 → hacktools-0.40.0}/hacktools/gui.py +0 -0
  24. {hacktools-0.38.7 → hacktools-0.40.0}/hacktools/nasm.py +0 -0
  25. {hacktools-0.38.7 → hacktools-0.40.0}/hacktools/nitro.py +0 -0
  26. {hacktools-0.38.7 → hacktools-0.40.0}/hacktools/wii.py +0 -0
  27. {hacktools-0.38.7 → hacktools-0.40.0}/hacktools/ws.py +0 -0
  28. {hacktools-0.38.7 → hacktools-0.40.0}/hacktools.egg-info/SOURCES.txt +0 -0
  29. {hacktools-0.38.7 → hacktools-0.40.0}/hacktools.egg-info/dependency_links.txt +0 -0
  30. {hacktools-0.38.7 → hacktools-0.40.0}/hacktools.egg-info/requires.txt +0 -0
  31. {hacktools-0.38.7 → hacktools-0.40.0}/hacktools.egg-info/top_level.txt +0 -0
  32. {hacktools-0.38.7 → hacktools-0.40.0}/setup.cfg +0 -0
  33. {hacktools-0.38.7 → hacktools-0.40.0}/tests/test_cmp.py +0 -0
@@ -1,17 +1,17 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hacktools
3
- Version: 0.38.7
3
+ Version: 0.40.0
4
4
  Summary: A set of utilities and tools for rom hacking and translations.
5
5
  Home-page: https://github.com/Illidanz/hacktools
6
6
  Author: Illidan
7
- Classifier: Programming Language :: Python :: 3.7
7
+ License: MIT
8
8
  Classifier: Programming Language :: Python :: 3.8
9
9
  Classifier: Programming Language :: Python :: 3.9
10
10
  Classifier: Programming Language :: Python :: 3.10
11
11
  Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Programming Language :: Python :: 3.13
14
- Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3.14
15
15
  Classifier: Operating System :: OS Independent
16
16
  Requires-Python: >=3.7
17
17
  Description-Content-Type: text/markdown
@@ -52,6 +52,7 @@ Dynamic: classifier
52
52
  Dynamic: description
53
53
  Dynamic: description-content-type
54
54
  Dynamic: home-page
55
+ Dynamic: license
55
56
  Dynamic: license-file
56
57
  Dynamic: provides-extra
57
58
  Dynamic: requires-python
@@ -0,0 +1 @@
1
+ __version__ = "0.40.0"
@@ -210,7 +210,10 @@ static PyObject* compressLZ10(PyObject* m, PyObject* args, PyObject* kwargs)
210
210
  if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s#i", kwlist, &indata, &inlength, &mindisp))
211
211
  return NULL;
212
212
 
213
- unsigned char* out = PyMem_Malloc(inlength);
213
+ // Worst case: uncompressible data emits 1 flag byte per 8 input bytes,
214
+ // so output can reach inlength + ceil(inlength/8)
215
+ size_t outcap = inlength + (inlength + 7) / 8 + 32;
216
+ unsigned char* out = PyMem_Malloc(outcap);
214
217
  MALLOC_CHECK(out);
215
218
 
216
219
  unsigned int compressedlength = 0;
@@ -288,7 +291,10 @@ static PyObject* compressLZ11(PyObject* m, PyObject* args, PyObject* kwargs)
288
291
  if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s#i", kwlist, &indata, &inlength, &mindisp))
289
292
  return NULL;
290
293
 
291
- unsigned char* out = PyMem_Malloc(inlength);
294
+ // Worst case: uncompressible data emits 1 flag byte per 8 input bytes,
295
+ // so output can reach inlength + ceil(inlength/8)
296
+ size_t outcap = inlength + (inlength + 7) / 8 + 32;
297
+ unsigned char* out = PyMem_Malloc(outcap);
292
298
  MALLOC_CHECK(out);
293
299
 
294
300
  unsigned int compressedlength = 0;
@@ -1171,7 +1171,7 @@ def repackBinaryStrings(section, infile, outfile, binranges, freeranges=None, re
1171
1171
  fo.writeZero(endpos - fo.tell())
1172
1172
  pos = fi.tell() - 1
1173
1173
  fi.seek(pos + 1)
1174
- return notfound
1174
+ return notfound, freeranges
1175
1175
 
1176
1176
 
1177
1177
  # Folders
@@ -152,14 +152,14 @@ def repackBIN(binrange, freeranges=[], readfunc=common.detectEncodedString, writ
152
152
  section.preloadLookup(comments)
153
153
  if type(binrange) == tuple:
154
154
  binrange = [binrange]
155
- notfound = common.repackBinaryStrings(section, binin, binout, binrange, freeranges, readfunc, writefunc, encoding, pointerstart, injectstart, fallbackf, injectfallback, sectionname, preformat, postformat)
155
+ notfound, freeranges = common.repackBinaryStrings(section, binin, binout, binrange, freeranges, readfunc, writefunc, encoding, pointerstart, injectstart, fallbackf, injectfallback, sectionname, preformat, postformat)
156
156
  for pointer in notfound:
157
157
  common.logError("Pointer", common.toHex(pointer.old), "->", common.toHex(pointer.new), "not found for string", pointer.str)
158
158
  if binfile.endswith(".txt"):
159
159
  common.logMessage("Done! Translation is at {0:.2f}%".format((100 * transtot) / chartot))
160
160
  else:
161
161
  common.logMessage("Done! Translation is at {0:.2f}%".format(section.getProgress()))
162
- return True
162
+ return freeranges
163
163
 
164
164
 
165
165
  class BINSection:
@@ -243,6 +243,168 @@ def readELF(infile):
243
243
  return elf
244
244
 
245
245
 
246
+ def expandELF(elfpath, n, fixheap=True):
247
+ # Inserts a code cave of n bytes at the end of the RWX PT_LOAD segment of a
248
+ # 32-bit ELF, fixing up all the headers so the file stays valid. Returns the
249
+ # virtual address of the cave (the old segment _end). When fixheap is set,
250
+ # MIPS references to _end used as the heap base are bumped past the cave.
251
+ # Read headers
252
+ with common.Stream(elfpath, "rb") as f:
253
+ e_phoff = f.readUIntAt(0x1c)
254
+ e_shoff = f.readUIntAt(0x20)
255
+ f.seek(0x2a)
256
+ e_phentsize = f.readUShort()
257
+ e_phnum = f.readUShort()
258
+ e_shentsize = f.readUShort()
259
+ e_shnum = f.readUShort()
260
+
261
+ phs = [] # (ph_off, p_type, p_offset, p_vaddr, p_paddr, p_filesz, p_memsz, p_flags, p_align)
262
+ for i in range(e_phnum):
263
+ ph_off = e_phoff + i * e_phentsize
264
+ f.seek(ph_off)
265
+ entry = (ph_off,
266
+ f.readUInt(), f.readUInt(), f.readUInt(), f.readUInt(),
267
+ f.readUInt(), f.readUInt(), f.readUInt(), f.readUInt())
268
+ phs.append(entry)
269
+
270
+ sections = [] # (sh_off, sh_addr, sh_offset, sh_size)
271
+ for i in range(e_shnum):
272
+ sh_off = e_shoff + i * e_shentsize
273
+ f.seek(sh_off + 0x0c)
274
+ sh_addr = f.readUInt()
275
+ sh_offset = f.readUInt()
276
+ sh_size = f.readUInt()
277
+ sections.append((sh_off, sh_addr, sh_offset, sh_size))
278
+
279
+ f.seek(0)
280
+ data = bytearray(f.read())
281
+
282
+ # Identify the RWX PT_LOAD segment that holds .text/.data
283
+ target = next((p for p in phs if p[1] == 1 and p[7] == 7), None)
284
+ if target is None:
285
+ common.logError("Could not find RWX PT_LOAD segment")
286
+ return 0
287
+ _, _, p_offset, p_vaddr, _, p_filesz, p_memsz, _, _ = target
288
+
289
+ # The cave will live at vaddr = p_vaddr + p_memsz, above all original BSS
290
+ # We need filesz to reach that vaddr and cover the cave, which means inserting
291
+ # (p_memsz - p_filesz) zero bytes for the BSS gap + n cave bytes
292
+ bss_gap = p_memsz - p_filesz
293
+ insert_total = bss_gap + n
294
+ insert_at = p_offset + p_filesz
295
+
296
+ # Splice the inserted bytes (all zero) into the file
297
+ data[insert_at:insert_at] = bytes(insert_total)
298
+ with common.Stream(elfpath, "wb") as f:
299
+ f.write(bytes(data))
300
+
301
+ # Patch headers (positions are post-splice)
302
+ new_shoff = e_shoff + insert_total if e_shoff >= insert_at else e_shoff
303
+ with common.Stream(elfpath, "r+b") as f:
304
+ # e_shoff
305
+ f.writeUIntAt(0x20, new_shoff)
306
+
307
+ # Program headers
308
+ for (ph_off, p_type, ph_p_offset, ph_p_vaddr, ph_p_paddr, ph_p_filesz, ph_p_memsz, ph_p_flags, ph_p_align) in phs:
309
+ # Program header table itself doesn't move (e_phoff < insert_at).
310
+ new_p_offset = ph_p_offset + insert_total if ph_p_offset >= insert_at else ph_p_offset
311
+ new_p_vaddr = ph_p_vaddr
312
+ new_p_paddr = ph_p_paddr
313
+ new_p_filesz = ph_p_filesz
314
+ new_p_memsz = ph_p_memsz
315
+ if (p_type, ph_p_flags) == (1, 7):
316
+ # The cave segment grows in both file and memory.
317
+ new_p_filesz = ph_p_filesz + insert_total
318
+ new_p_memsz = ph_p_memsz + n
319
+ elif ph_p_vaddr >= p_vaddr + p_memsz:
320
+ # Any segment that lived at/above the old memsz boundary (e.g. PH1)
321
+ # gets shifted up by the cave size so it doesn't overlap
322
+ new_p_vaddr = ph_p_vaddr + n
323
+ new_p_paddr = ph_p_paddr + n
324
+ f.writeUIntAt(ph_off + 0x04, new_p_offset)
325
+ f.writeUIntAt(ph_off + 0x08, new_p_vaddr)
326
+ f.writeUIntAt(ph_off + 0x0c, new_p_paddr)
327
+ f.writeUIntAt(ph_off + 0x10, new_p_filesz)
328
+ f.writeUIntAt(ph_off + 0x14, new_p_memsz)
329
+
330
+ # Section headers
331
+ for sh_off, sh_addr, sh_offset, sh_size in sections:
332
+ new_addr, new_offset, new_size = sh_addr, sh_offset, sh_size
333
+ if sh_offset >= insert_at:
334
+ new_offset = sh_offset + insert_total
335
+ elif sh_offset != 0 and sh_size != 0 and sh_offset + sh_size == insert_at:
336
+ new_size = sh_size + insert_total
337
+ if sh_addr >= p_vaddr + p_memsz:
338
+ new_addr = sh_addr + n
339
+ new_sh_off = sh_off + insert_total if sh_off >= insert_at else sh_off
340
+ f.writeUIntAt(new_sh_off + 0x0c, new_addr)
341
+ f.writeUIntAt(new_sh_off + 0x10, new_offset)
342
+ f.writeUIntAt(new_sh_off + 0x14, new_size)
343
+
344
+ # Bump references to the original _end (= old PT_LOAD memsz boundary) that
345
+ # are used as the heap base. If we don't shift them, the game's first
346
+ # malloc/sbrk lands on top of our cave
347
+ old_end = p_vaddr + p_memsz
348
+ new_end = old_end + n
349
+ if fixheap:
350
+ sec_lo = p_offset
351
+ sec_hi = p_offset + p_filesz + insert_total
352
+ with common.Stream(elfpath, "r+b") as f:
353
+ # Literals: any 4-byte word equal to old_end gets bumped.
354
+ for off in range(sec_lo, sec_hi - 3, 4):
355
+ if f.readUIntAt(off) == old_end:
356
+ f.writeUIntAt(off, new_end)
357
+ # lui+addiu/ori pairs producing old_end, followed by a syscall nearby
358
+ for off in range(sec_lo, sec_hi - 4, 4):
359
+ w = f.readUIntAt(off)
360
+ if (w >> 26) != 0x0f:
361
+ continue
362
+ rt = (w >> 16) & 0x1f
363
+ hi = (w & 0xffff) << 16
364
+ for j in range(1, 9):
365
+ off2 = off + j * 4
366
+ if off2 >= sec_hi:
367
+ break
368
+ w2 = f.readUIntAt(off2)
369
+ op2 = w2 >> 26
370
+ rs2 = (w2 >> 21) & 0x1f
371
+ if rs2 != rt:
372
+ continue
373
+ if op2 == 0x09:
374
+ lo = w2 & 0xffff
375
+ if lo >= 0x8000:
376
+ lo -= 0x10000
377
+ addr = (hi + lo) & 0xffffffff
378
+ elif op2 == 0x0d:
379
+ addr = hi | (w2 & 0xffff)
380
+ else:
381
+ continue
382
+ if addr != old_end:
383
+ break
384
+ # Look ahead up to 3 instructions for a "syscall" (opcode=0, funct=0x0c)
385
+ # Only the heap-setup site matches this
386
+ has_syscall = False
387
+ for k in range(1, 5):
388
+ off3 = off2 + k * 4
389
+ if off3 >= sec_hi:
390
+ break
391
+ w3 = f.readUIntAt(off3)
392
+ if (w3 >> 26) == 0 and (w3 & 0x3f) == 0x0c:
393
+ has_syscall = True
394
+ break
395
+ if not has_syscall:
396
+ break
397
+ new_hi = (new_end >> 16) & 0xffff
398
+ new_lo = new_end & 0xffff
399
+ if op2 == 0x09 and (new_lo & 0x8000):
400
+ new_hi = (new_hi + 1) & 0xffff
401
+ f.writeUIntAt(off, (w & 0xffff0000) | new_hi)
402
+ f.writeUIntAt(off2, (w2 & 0xffff0000) | new_lo)
403
+ break
404
+ common.logDebug("Expanded ELF, cave at", common.toHex(old_end), "size", common.toHex(n))
405
+ return old_end
406
+
407
+
246
408
  def extractBinaryStrings(elf, foundstrings, infile, func, encoding="shift_jis", elfsections=[".rodata"]):
247
409
  with common.Stream(infile, "rb") as f:
248
410
  for sectionname in elfsections:
@@ -7,13 +7,13 @@ from hacktools import common
7
7
  # Image functions
8
8
  def extractBIN(infolder, outfolder, cuefile):
9
9
  try:
10
- import pymkpsxiso
10
+ import pydumpsxiso
11
11
  except ImportError:
12
- common.logError("pymkpsxiso not found")
12
+ common.logError("pydumpsxiso not found")
13
13
  return
14
14
  common.logMessage("Extracting BIN", cuefile, "...")
15
15
  common.makeFolder(infolder)
16
- pymkpsxiso.dump(cuefile.replace(".cue", ".bin"), infolder[:-1], infolder[:-1] + ".xml")
16
+ pydumpsxiso.run(cuefile.replace(".cue", ".bin"), infolder[:-1], infolder[:-1] + ".xml")
17
17
  common.logMessage("Copying data to", outfolder, "...")
18
18
  common.copyFolder(infolder, outfolder)
19
19
  with open(infolder[:-1] + ".xml", "r") as f:
@@ -30,7 +30,7 @@ def repackBIN(infolder, binin, binout, cuefile, patchfile=""):
30
30
  common.logError("pymkpsxiso not found")
31
31
  return
32
32
  common.logMessage("Repacking BIN", binout, "...")
33
- pymkpsxiso.make(binout, cuefile, infolder[:-1] + ".xml")
33
+ pymkpsxiso.run(binout, cuefile, infolder[:-1] + ".xml")
34
34
  common.logMessage("Done!")
35
35
  # Create xdelta patch
36
36
  if patchfile != "":
@@ -64,7 +64,7 @@ def repackEXE(binrange, freeranges=None, manualptrs=None, readfunc=common.detect
64
64
  chartot, transtot = common.getSectionPercentage(section)
65
65
  if type(binrange) == tuple:
66
66
  binrange = [binrange]
67
- notfound = common.repackBinaryStrings(section, exein, exeout, binrange, freeranges, readfunc, writefunc, encoding, 0x8000f800)
67
+ notfound, freeranges = common.repackBinaryStrings(section, exein, exeout, binrange, freeranges, readfunc, writefunc, encoding, 0x8000f800)
68
68
  # Handle not found pointers by manually replacing the opcodes
69
69
  if len(notfound) > 0 and manualptrs is not None:
70
70
  with open(ptrfile, "w") as f:
@@ -1,17 +1,17 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hacktools
3
- Version: 0.38.7
3
+ Version: 0.40.0
4
4
  Summary: A set of utilities and tools for rom hacking and translations.
5
5
  Home-page: https://github.com/Illidanz/hacktools
6
6
  Author: Illidan
7
- Classifier: Programming Language :: Python :: 3.7
7
+ License: MIT
8
8
  Classifier: Programming Language :: Python :: 3.8
9
9
  Classifier: Programming Language :: Python :: 3.9
10
10
  Classifier: Programming Language :: Python :: 3.10
11
11
  Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Programming Language :: Python :: 3.13
14
- Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3.14
15
15
  Classifier: Operating System :: OS Independent
16
16
  Requires-Python: >=3.7
17
17
  Description-Content-Type: text/markdown
@@ -52,6 +52,7 @@ Dynamic: classifier
52
52
  Dynamic: description
53
53
  Dynamic: description-content-type
54
54
  Dynamic: home-page
55
+ Dynamic: license
55
56
  Dynamic: license-file
56
57
  Dynamic: provides-extra
57
58
  Dynamic: requires-python
@@ -43,15 +43,15 @@ setup(
43
43
  Extension("hacktools.cmp_misc", sources=["hacktools/c_ext/cmp_misc.c"]),
44
44
  Extension("hacktools.cmp_racjin", sources=["hacktools/c_ext/cmp_racjin.c"]),
45
45
  ],
46
+ license="MIT",
46
47
  classifiers=[
47
- "Programming Language :: Python :: 3.7",
48
48
  "Programming Language :: Python :: 3.8",
49
49
  "Programming Language :: Python :: 3.9",
50
50
  "Programming Language :: Python :: 3.10",
51
51
  "Programming Language :: Python :: 3.11",
52
52
  "Programming Language :: Python :: 3.12",
53
53
  "Programming Language :: Python :: 3.13",
54
- "License :: OSI Approved :: MIT License",
54
+ "Programming Language :: Python :: 3.14",
55
55
  "Operating System :: OS Independent",
56
56
  ],
57
57
  extras_require = {
@@ -1 +0,0 @@
1
- __version__ = "0.38.7"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes