lbf2tar.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. #!/usr/local/bin/python3
  2. """
  3. Copyright (C) All Rights Reserved
  4. Written by Wazakindjes
  5. Website: https://gitgud.malvager.net/Wazakindjes/lg-lbf-extractor
  6. License: https://gitgud.malvager.net/Wazakindjes/lg-lbf-extractor/raw/master/LICENSE
  7. """
  8. import mmap
  9. import os
  10. import sys
  11. DEBUG = False # If true, output additional messages
  12. DEBUG_MAX = 10 # Stop after up to DEBUG_MAX files lol (0 to disable)
  13. DEBUG_SKIP = 10 # Skip the first N files (0 to disable)
  14. # Better not touch these my mane
  15. MUHVERSION = 'v1.10 b20180805'
  16. MAXZIP = 54
  17. ZIPHEAD = b'PK\x03\x04\x14' # \x50\x4B\x03\x04\x14
  18. ZIPTAIL = b'PK\x05\x06\x00' # \x50\x4B\x05\x06\x00
  19. MUHEXT = '.tar'
  20. USTARHEAD = b'\x75\x73\x74\x61\x72\x20\x20\x00'
  21. USTARTAIL = b'' # USTAR should always end with 1024 null bytes =]
  22. for i in range(0, 1024):
  23. USTARTAIL += b'\x00'
  24. def gibsize(byets):
  25. suffix = 'B'
  26. sizelol = byets
  27. while sizelol >= 1024:
  28. if suffix == 'B':
  29. suffix = 'kiB'
  30. elif suffix == 'kiB':
  31. suffix = 'MiB'
  32. elif suffix == 'MiB':
  33. suffix = 'GiB'
  34. sizelol /= 1024
  35. return (sizelol, suffix)
  36. if len(sys.argv) < 3 or sys.argv[1].lower() in ['h', 'halp', 'help', '-h', '--halp', '--help']:
  37. print(f"Usage: {sys.argv[0]} <LGBackup*.lbf file path> <extraction directory>")
  38. print(f"\tThis tool will extract concatenated ZIP/USTAR files from the specified .lbf file into <extraction dir>.")
  39. print(f"\tEvery ZIP/USTAR will be transformed into a zero-padded numbered {MUHEXT} file cuz I feel tar > unzip for broken archives. ;]")
  40. print(f"\tVersion: {MUHVERSION}")
  41. sys.exit(0)
  42. lbfpath = sys.argv[1]
  43. tdir = sys.argv[2]
  44. print(f"** Gonna extract '{lbfpath}' into '{tdir}'")
  45. if not os.path.exists(tdir): # Make sure the directories exist yo
  46. os.makedirs(tdir)
  47. maxcount = { 'ZIP': 0, 'USTAR': 0 } # Count em separately obv
  48. maxwidth = 0 # One value for both ;]
  49. filesize = 0 # File itself lol
  50. muhpos = [] # Gonna store em positions yo
  51. print("** But will first count the amount of special file headers, starting with some ZIPs")
  52. with open(lbfpath, "rb+") as lbf:
  53. # Need mmap here because just lbf.find would return an AttributeError: '_io.BufferedRandom' object has no attribute 'find'
  54. mm = mmap.mmap(lbf.fileno(), 0) # Map entire file into mamm0ry =]
  55. p1 = -1 # For easy logic below
  56. dskip = { 'ZIP': 0, 'USTAR': 0 }
  57. for i in range(1, MAXZIP + 1):
  58. p1 = mm.find(ZIPHEAD, p1 + 1) # Make sure the next search starts after the 'P'
  59. p2 = mm.find(ZIPTAIL, p1 + 1)
  60. if p1 < 0 or p2 < 0: # No more headers
  61. break
  62. if DEBUG and DEBUG_SKIP > 0 and dskip['ZIP'] < DEBUG_SKIP: # Maybe we need to skip the first N?
  63. print(f"\t* DEBUG: Skipping #{i} due to DEBUG_SKIP ({DEBUG_SKIP})")
  64. dskip['ZIP'] += 1
  65. continue
  66. maxcount['ZIP'] += 1
  67. muhpos.append(['ZIP', p1, p2]) # Store this file slice imo tbh
  68. if DEBUG and DEBUG_MAX > 0 and maxcount['ZIP'] >= DEBUG_MAX: # Maybe we got enough already?
  69. print(f"\t* DEBUG: Skipping the rest due to DEBUG_MAX ({DEBUG_MAX})")
  70. break
  71. print(f"\t- Found {maxcount['ZIP']} (apparent) files")
  72. print("** USTAR is up next y0")
  73. p1 = -1
  74. while True:
  75. # Let's look between USTAR headers imo tbh fam (thanks LG)
  76. p1 = mm.find(USTARHEAD, p1 + 1)
  77. p2 = mm.find(USTARHEAD, p1 + 1)
  78. if p1 < 0 or p2 < 0: # No more headers
  79. break
  80. if DEBUG and DEBUG_SKIP > 0 and dskip['USTAR'] < DEBUG_SKIP: # Every file type has their own skip limits =]
  81. dskip['USTAR'] += 1
  82. print(f"\t* DEBUG: Skipping #{dskip['USTAR']} due to DEBUG_SKIP ({DEBUG_SKIP})")
  83. continue
  84. maxcount['USTAR'] += 1
  85. muhpos.append(['USTAR', p1, p2])
  86. if DEBUG and DEBUG_MAX > 0 and maxcount['USTAR'] >= DEBUG_MAX:
  87. print(f"\t* DEBUG: Skipping the rest due to DEBUG_MAX ({DEBUG_MAX})")
  88. break
  89. print(f"\t- Found {maxcount['USTAR']} (apparent) files")
  90. # Get max filesize lol
  91. lbf.seek(0, 2)
  92. filesize = lbf.tell()
  93. if maxcount['ZIP'] <= 0 and maxcount['USTAR'] <= 0:
  94. print("** No valid headers found at all, bailing out")
  95. sys.exit(1)
  96. # Get total size in human readable format etc
  97. (f_sizelol, f_suffix) = gibsize(filesize)
  98. print(f"** Beginning extraction of {f_sizelol:.2f} {f_suffix} backup file ({filesize} B)")
  99. maxwidth = len(str(DEBUG_SKIP + maxcount['ZIP'] + maxcount['USTAR']))
  100. totalbyets = { 'ZIP': 0, 'USTAR': 0, 'grand': 0 }
  101. count = DEBUG_SKIP
  102. with open(lbfpath, "rb+") as lbf:
  103. mm = mmap.mmap(lbf.fileno(), 0)
  104. for posinfo in muhpos:
  105. ext = posinfo[0]
  106. p1 = posinfo[1]
  107. p2 = posinfo[2]
  108. count += 1
  109. base = str(count)
  110. if len(base) < maxwidth:
  111. base = base.zfill(maxwidth)
  112. muhpath = f"{tdir}/{base}{MUHEXT}"
  113. byets = 0 # Total read for this file etc
  114. chunksize = 512 # Default value for USTAR
  115. if ext == 'USTAR':
  116. lbf.seek(p1 - 257) # Let's include (possibly br0kne) USTAR header lol
  117. else:
  118. lbf.seek(p1) # Otherwise just go to ZIPHEAD
  119. chunksize = p2 - p1 # Read everything at once imo
  120. # Gotta open the target file before reading the LBF slice cuz we write in chunks my mane
  121. print(f"\t- Writing: {base}{MUHEXT}... ", end='', flush=True)
  122. with open(muhpath, "wb") as datarchive:
  123. while True:
  124. # Make sure a USTAR doesn't get bytes from the next one
  125. if p2 - p1 - byets < chunksize:
  126. chunksize = p2 - p1 - byets
  127. if chunksize <= 0: # File happened to be cleanly divisible by 512 :>
  128. break
  129. data = lbf.read(chunksize)
  130. if not data:
  131. print(f"UNEXPECTED ERROR: no data returned from lbf.read()")
  132. if DEBUG:
  133. print(f"\t\t* DEBUG: p1 = {p1}, p2 = {p2}, chunksize = {chunksize}, byets = {byets}")
  134. break
  135. # Write the chunk to target file
  136. byets += chunksize
  137. datarchive.write(data)
  138. if ext == 'USTAR' and data == USTARTAIL: # Maybe we sometimes get a proper archive ending with 1024 bytes (2 blocks of 512) of \x00
  139. chunksize = 0
  140. # Gotta make sure our USTAR follows the spec ;];];];]
  141. if ext == 'USTAR':
  142. # It seems LG's format does end with some nullbytes, so we just have to pad 1024 - what they did
  143. remainder = byets % 512
  144. if remainder > 0:
  145. if DEBUG:
  146. print(f" [inserting {remainder} null bytes] ", end='', flush=True)
  147. byets += remainder
  148. nullem = b''
  149. for i in range(0, remainder):
  150. nullem += b'\x00'
  151. datarchive.write(nullem)
  152. # Print some size info for dis slice if we got ne
  153. if byets > 0:
  154. totalbyets[ext] += byets
  155. totalbyets['grand'] += byets
  156. (sizelol, suffix) = gibsize(byets)
  157. (t_sizelol, t_suffix) = gibsize(totalbyets[ext])
  158. print(f" {sizelol:.2f} {suffix}, total {ext} size read so far: {t_sizelol:.2f} {t_suffix}")
  159. if DEBUG:
  160. print(f"\t\t* DEBUG: p1 = {p1}, p2 = {p2}, byets = {byets}")
  161. (gt_sizelol, gt_suffix) = gibsize(totalbyets['grand'])
  162. print(f"** Aye we done writing {gt_sizelol:.2f} {gt_suffix} worth of data =]]]")