Advertisement
Guest User

catpack version "works slightly better"

a guest
Jun 3rd, 2016
4,225
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Ruby 9.89 KB | None | 0 0
  1. require 'csv'
  2. require 'tempfile'
  3. require 'fileutils'
  4.  
  5. def getStringLength(f)
  6.     original_offset = f.tell()
  7.    
  8.     len = 0
  9.     while f.readbyte != 0x00 do
  10.         len += 1
  11.     end
  12.     len += 1 # null terminator
  13.    
  14.     f.seek(original_offset)
  15.     return len
  16. end
  17.  
  18. # monkeypatching shenanigans
  19. class File
  20.     def read_int
  21.         read(4).unpack("I")[0]
  22.     end
  23.    
  24.     def read_string
  25.         read(getStringLength(self)).unpack("Z*")[0]
  26.     end
  27.    
  28.     def write_int(i)
  29.         write([i].pack("I"))
  30.     end
  31. end
  32. # end shenanigans
  33.  
  34. class CSV_file_record
  35.     attr_reader :tag, :semantic
  36.     attr_reader :filenames
  37.    
  38.     def initialize(row)
  39.         @tag = row[0]
  40.         @semantic = row[1] # don't know what this does.
  41.        
  42.         @filenames = []
  43.     end
  44.    
  45.     def add_filename(str)
  46.         @filenames << str
  47.     end
  48. end
  49.  
  50. # so I had assumed that the CSV string was null-terminated
  51. # originally, but some archives coincidentally have the
  52. # end of the CSV string butting right up against the start
  53. # of the actual file data, with no null terminator separating
  54. # the two. as a stopgap, I'll just clip the string till the last newline
  55. def unfuck_csv(str)
  56.     last_newline = str.rindex("0A".hex.chr)
  57.     str[0..last_newline]
  58. end
  59.  
  60. class CSV_file_table
  61.     attr_reader :files
  62.     attr_reader :csv_table_string
  63.    
  64.     def initialize(file)
  65.         file.seek(file.read_int) # seek to CSV table offset (the first int in the CAT)
  66.         # NEW AND IMPROVED because what the fuck???
  67.         @csv_table_string = unfuck_csv(file.read_string)
  68.        
  69.         raw_csv = CSV.parse(@csv_table_string)
  70.        
  71.         @files = []        
  72.         current_file = nil
  73.        
  74.         raw_csv.each do |row|
  75.            if row[0] != nil # new file defined
  76.                current_file = CSV_file_record.new(row)
  77.                @files << current_file
  78.            end          
  79.            current_file.add_filename(row[2])
  80.         end
  81.        
  82.         print "Loaded CSV table containing #{@files.count} outer files.\n"
  83.     end
  84. end
  85.  
  86. def unpack_cat(filename)
  87.     File.open(filename, 'rb') do |f|
  88.         filetable = CSV_file_table.new(f)
  89.        
  90.         folder_name = filename[0..-(File.extname(filename).length+1)]
  91.         FileUtils::mkdir_p(folder_name)
  92.        
  93.         filetable.files.each_with_index do |file, outer_index|
  94.             f.seek(4 + outer_index*4)
  95.             f.seek(f.read_int)
  96.            
  97.             file_base = f.tell
  98.            
  99.             if file.tag == "GXT"
  100.                 f.seek(file_base)  
  101.                 if (f.read_int == 0x00545847) # GXT
  102.                     # fuck it
  103.                     puts "Encountered a GXT file. This type of file is not supported; aborting."
  104.                     exit
  105.                 end
  106.                 f.seek(file_base)
  107.                 header_size = f.read_int
  108.                 file_count = f.read_int
  109.                 archive_size = f.read_int
  110.                
  111.                 gxt_ptr_table_off = f.tell
  112.                 gxt_file_offsets = []
  113.                 file_count.times { gxt_file_offsets << f.read_int }
  114.                 f.seek(file_base + header_size)
  115.                 data_base = f.tell
  116.                
  117.                 file.filenames.each_with_index do |inner_name, inner_index|
  118.                     # pull filename from third CSV entry
  119.                     out_filename = inner_name
  120.                     out_filename += ".dds"
  121.                     File.open(folder_name + '/' + out_filename, 'wb') do |outfile|
  122.                         f.seek(file_base + header_size + gxt_file_offsets[inner_index])
  123.                        
  124.                         image_base = f.tell
  125.                         #f.seek(image_base + 0xC)
  126.                         #dimensions = f.read(8).unpack("II")
  127.                        
  128.                         print out_filename + ", "
  129.                         print "0#{image_base.to_s(16)}"
  130.                         print "\n"
  131.                        
  132.                         #image_filesize = 0x4 + 0x7C + (dimensions[0] * dimensions[1])
  133.                         image_filesize = -1
  134.                         # if this is the last file then we calculate filesize differently
  135.                         if inner_index == file.filenames.count - 1
  136.                             image_filesize = (file_base + archive_size) - image_base
  137.                         else
  138.                             image_filesize = gxt_file_offsets[inner_index + 1] - gxt_file_offsets[inner_index]
  139.                         end
  140.                         f.seek(image_base)
  141.                        
  142.                         outfile.write(f.read(image_filesize))
  143.                     end
  144.                 end
  145.             else
  146.                 # unknown tag, just dump the raw data
  147.                 f.seek(4 + (outer_index+1)*4)
  148.                 # subtract file offset from offset of next file to get
  149.                 # approximate file size (CAT archive doesn't seem to
  150.                 # know the sizes of the files inside it; just their offsets)
  151.                 next_ptr = f.read_int
  152.                 # i don't know if reading past EOF breaks things in ruby
  153.                 # and i'm too lazy to figure it out
  154.                 next_ptr = File.size(filename) if next_ptr == 0xFFFFFFFF
  155.                 filesize = next_ptr - file_base
  156.                
  157.                 out_filename = file.semantic + "." + file.tag
  158.                 File.open(folder_name + '/' + out_filename, 'wb') do |outfile|
  159.                     f.seek(file_base)
  160.                     outfile.write(f.read(filesize))
  161.                     puts "Dumped #{filesize.to_s(16)} bytes of unknown data from #{file_base} to #{out_filename}."
  162.                 end
  163.             end
  164.         end
  165.     end
  166. end
  167.  
  168. def padded_to(value, interval)
  169.     return value if (value % interval == 0)    
  170.     return value + (interval - (value % interval) )
  171. end
  172.  
  173. def repack_cat(filename)
  174.     filetable = nil
  175.     File.open(filename, 'rb') do |f|
  176.         filetable = CSV_file_table.new(f)
  177.     end
  178.    
  179.     folder_name = filename[0..-(File.extname(filename).length+1)]
  180.     if !File.exist?(folder_name)
  181.         puts "No valid directory found to repack."
  182.         puts "(Directory #{folder_name} is either a file or is nonexistent.)"
  183.         exit
  184.     end
  185.    
  186.     fileblobs = []
  187.     filetable.files.each_with_index do |file, outer_index|
  188.         blob = nil
  189.         if file.tag == "GXT"
  190.             # assemble a GXT using files in the unpacked directory
  191.             t = Tempfile.new('gxt')
  192.             t.binmode
  193.            
  194.             # write temporary header
  195.             3.times { t.write_int(0) }
  196.            
  197.             # write temporary offset table
  198.             file.filenames.count.times { t.write_int(0) }
  199.            
  200.             file_data_base = t.tell
  201.             offsets = []
  202.             file.filenames.each do |name|
  203.                 offsets << t.tell - file_data_base
  204.                 target_path = folder_name + '/' + name + '.dds'
  205.                 if !File.exists?(target_path)
  206.                     puts "Target file #{target_path} doesn't exist!"
  207.                     # TODO: should probably just grab the file from the original archive but I can't be arsed
  208.                 end
  209.                 File.open(target_path, 'rb') do |target|
  210.                     dat = target.read
  211.                     t.write(dat)
  212.                 end
  213.             end
  214.            
  215.             archive_size = t.tell
  216.            
  217.             # now write real header and offset table
  218.             t.seek(0)
  219.             t.write_int(file_data_base)
  220.             t.write_int(file.filenames.count)
  221.             t.write_int(archive_size)            
  222.             offsets.each do
  223.                 |offset| t.write_int(offset)
  224.             end
  225.                
  226.             # finally, shove the GXT contents into our blob
  227.             t.seek(0)
  228.             blob = t.read
  229.             t.seek(0)
  230.            
  231.             t.close
  232.             t.unlink
  233.         else
  234.             # create a binary blob and stick that into the new file
  235.             out_filename = file.semantic + "." + file.tag
  236.             File.open(folder_name + '/' + out_filename, 'rb') do |target|
  237.                 blob = target.read
  238.             end
  239.         end
  240.        
  241.         fileblobs << blob
  242.     end
  243.    
  244.     # we have an array of file blobs, now write a CAT
  245.     # first, make a backup
  246.     FileUtils.cp(filename, filename + '.backup') unless File.exists?(filename + '.backup')
  247.     if !File.exists?(filename + '.backup')
  248.         puts "Failed to create backup; aborting."
  249.         exit
  250.     end
  251.    
  252.     File.open(filename, 'wb') do |f|
  253.         # write temp header
  254.         f.write_int(0)
  255.         # write temp offset table
  256.         filetable.files.count.times { f.write_int(0) }
  257.         f.write_int(0xFFFFFFFF) # write end pointer
  258.        
  259.         # the CAT archives seem to pad their entries to 0x40,
  260.         # so I'll do the same
  261.         csv_offset = padded_to(f.tell, 0x40)
  262.         # write csv data thing
  263.         f.seek(csv_offset)
  264.         f.write(filetable.csv_table_string)
  265.        
  266.         offsets = []
  267.         fileblobs.each do |blob|
  268.             f.seek(padded_to(f.tell, 0x40))
  269.             offsets << f.tell
  270.            
  271.             f.write(blob)
  272.         end
  273.        
  274.         # now write the real header
  275.         f.seek(0)
  276.         f.write_int(csv_offset)
  277.         # and the offset table
  278.         offsets.each do |offset|
  279.             f.write_int(offset)
  280.         end
  281.     end
  282.    
  283.     # done
  284. end
  285.  
  286. filename = ""
  287.  
  288. if ARGV.count < 2
  289.     puts "Not enough arguments. Specify an operation (unpack/repack) and path to target archive."
  290.     exit
  291. end
  292.  
  293. operation = ARGV[0]
  294. filename = ARGV[1]
  295.  
  296. if filename == ""
  297.     puts "No file specified."
  298. end
  299.  
  300. if operation == "unpack" || operation == "u"
  301.     unpack_cat(filename)
  302. elsif operation == "repack" || operation == "r"
  303.     repack_cat(filename)
  304. else
  305.     puts "Unknown operation #{operation} specified."
  306.     exit
  307. end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement