cia-unix/cia-unix.cr
Joshua Murphy 26b758a723 Replace shell backticks with Process.new for better escaping
This avoids issues with filenames containing spaces and improves reliability
when passing arguments to external tools.
2025-05-19 00:14:53 -04:00

170 lines
5.1 KiB
Crystal

require "colorize"
LOG = File.new "cia-unix.log", "w"
LOG.puts Time.utc.to_s
# dependencies check
tools = ["./ctrtool", "./ctrdecrypt", "./makerom", "seeddb.bin"]
tools.each do |tool|
case tool
when "./ctrtool", "./ctrdecrypt", "./makerom"
if !File.exists? %x[which #{tool}].chomp
LOG.delete if File.exists? "cia-unix.log"
download_dep
abort "#{tool.lchop("./").colorize.mode(:bold)} not found. Make sure it's located in the #{"same directory".colorize.mode(:underline)}" if !File.exists? tool
end
when "seeddb.bin"
if !File.exists? tool
LOG.delete if File.exists? "cia-unix.log"
download_dep
abort "#{tool.colorize.mode(:bold)} not found. Make sure it's located in the #{"same directory".colorize.mode(:underline)}" if !File.exists? tool
end
end
end
def download_dep
print "Some #{"tools".colorize.mode(:bold)} are missing, do you want to download them? (y/n): "
if ["y", "Y"].includes? gets.to_s
system "./dltools.sh"
end
end
# roms presence check
if Dir["*.cia"].size.zero? && Dir["*.3ds"].size.zero?
LOG.delete if File.exists? "cia-unix.log"
abort "No #{"CIA".colorize.mode(:bold)}/#{"3DS".colorize.mode(:bold)} roms were found."
end
def run_tool(name : String, args : Array(String)) : String
process = Process.new("./#{name}", args: args, output: Process::Redirect::Pipe)
content = process.output.gets_to_end
LOG.puts content
exit_code = process.wait.exit_code
raise "#{name} failed with exit code #{exit_code}" if exit_code != 0
content
end
def check_decrypt(name : String, ext : String)
if File.exists? "#{name}-decrypted.#{ext}"
puts "Decryption completed\n".colorize.mode(:underline)
else
puts "Decryption failed\n".colorize.mode(:underline)
end
end
def gen_args(name : String, part_count : Int32) : Array(String)
args = [] of String
part_count.times do |partition|
if File.exists? "#{name}.#{partition}.ncch"
args += ["-i", "#{name}.#{partition}.ncch:#{partition}:#{partition}"]
end
end
return args
end
# cache cleanup
def remove_cache
puts "Removing cache..."
Dir["*-decfirst.cia"].each do |fname| File.delete(fname) end
Dir["*.ncch"].each do |fname| File.delete(fname) end
end
# 3ds decrypting
Dir["*.3ds"].each do |ds|
next if ds.includes? "decrypted"
i : UInt8 = 0
dsn : String = ds.chomp ".3ds"
args = ["-f", "cci", "-ignoresign", "-target", "p", "-o", "#{dsn}-decrypted.3ds"]
puts "Decrypting: #{ds.colorize.mode(:bold)}..."
run_tool("ctrdecrypt", [ds])
Dir["#{dsn}.*.ncch"].each do |ncch|
case ncch
when "#{dsn}.Main.ncch"
i = 0
when "#{dsn}.Manual.ncch"
i = 1
when "#{dsn}.DownloadPlay.ncch"
i = 2
when "#{dsn}.Partition4.ncch"
i = 3
when "#{dsn}.Partition5.ncch"
i = 4
when "#{dsn}.Partition6.ncch"
i = 5
when "#{dsn}.N3DSUpdateData.ncch"
i = 6
when "#{dsn}.UpdateData.ncch"
i = 7
end
args += ["-i", "#{ncch}:#{i}:#{i}"]
end
puts "Building decrypted #{dsn} 3DS..."
run_tool("makerom", args)
check_decrypt(dsn, "3ds")
remove_cache
end
# cia decrypting
Dir["*.cia"].each do |cia|
next if cia.includes? "decrypted"
puts "Decrypting: #{cia.colorize.mode(:bold)}..."
cutn : String = cia.chomp ".cia"
content = run_tool("ctrtool", ["--seeddb=seeddb.bin", cia])
# game
if content.match /T.*d.*00040000/
puts "CIA Type: Game"
run_tool("ctrdecrypt", [cia])
args = ["-f", "cia", "-ignoresign", "-target", "p", "-o", "#{cutn}-decfirst.cia"]
i : UInt8 = 0
Dir["*.ncch"].sort.each do |ncch|
args += ["-i", "#{ncch}:#{i}:#{i}"]
i += 1
end
run_tool("makerom", args)
# patch
elsif content.match /T.*d.*0004000(e|E)/
puts "CIA Type: #{"Patch".colorize.mode(:bold)}"
run_tool("ctrdecrypt", [cia])
args = ["-f", "cia", "-ignoresign", "-target", "p", "-o", "#{cutn} (Patch)-decrypted.cia"]
patch_parts : Int32 = Dir["#{cutn}.*.ncch"].size
args += gen_args(cutn, patch_parts)
run_tool("makerom", args)
check_decrypt("#{cutn} (Patch)", "cia")
# dlc
elsif content.match /T.*d.*0004008(c|C)/
puts "CIA Type: #{"DLC".colorize.mode(:bold)}"
run_tool("ctrdecrypt", [cia])
args = ["-f", "cia", "-dlc", "-ignoresign", "-target", "p", "-o", "#{cutn} (DLC)-decrypted.cia"]
dlc_parts : Int32 = Dir["#{cutn}.*.ncch"].size
args += gen_args(cutn, dlc_parts)
run_tool("makerom", args)
check_decrypt("#{cutn} (DLC)", "cia")
else
puts "Unsupported CIA"
end
Dir["*-decfirst.cia"].each do |decfirst|
cutn = decfirst.chomp "-decfirst.cia"
puts "Building decrypted #{cutn} CCI..."
run_tool("makerom", ["-ciatocci", decfirst, "-o", "#{cutn}-decrypted.cci"])
check_decrypt(cutn, "cci")
end
remove_cache
end
LOG.flush
LOG.close
puts "Log saved"