Created
March 27, 2024 18:13
-
-
Save x4fx77x4f/927311c7601ce4870aee722f5856138a to your computer and use it in GitHub Desktop.
Picotron .p64.rom extraction and creation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env luajit | |
local argparse = require("argparse")(nil, [[Create a Picotron .p64.rom. | |
Dependencies: | |
sudo apt install liblz4-1 luajit luarocks | |
luarocks install luaposix --local]]) | |
argparse:argument("input_path", "Directory to read files from.") | |
argparse:argument("output_path", "Path to save .p64.rom to.") | |
local arguments = argparse:parse() | |
local input_path = arguments.input_path | |
local output_path = arguments.output_path | |
local output_file = assert(io.open(output_path, "w+b")) | |
local bit = require("bit") | |
local ffi = require("ffi") | |
local posix_dirent = require("posix.dirent") | |
local posix_sys_stat = require("posix.sys.stat") | |
local printf_file = io.stdout | |
if printf_file == output_file then | |
printf_file = io.stderr | |
end | |
local function printf(...) | |
return assert(printf_file:write(string.format(...))) | |
end | |
ffi.cdef([[ | |
const char *LZ4_versionString(void); | |
int LZ4_compress_default(const char *, char *, int, int); | |
]]) | |
local C = ffi.load("lz4") | |
printf("ffi.string(C.LZ4_versionString()): %q\n", ffi.string(C.LZ4_versionString())) | |
local function write_u8(stream, data) | |
return assert(stream:write(string.char(data))) | |
end | |
local function write_u32_le(stream, data) | |
assert(data >= 0 and data <= 2^32-1 and data%1 == 0) | |
return assert(stream:write(string.char( | |
bit.band(data, 0xff), | |
bit.band(bit.rshift(data, 8), 0xff), | |
bit.band(bit.rshift(data, 16), 0xff), | |
bit.rshift(data, 24) | |
))) | |
end | |
local function write_c_string(stream, data) | |
assert(string.find(data, "\x00", 1, true) == nil) | |
assert(stream:write(data)) | |
return assert(stream:write("\x00")) | |
end | |
local uncompressed = {} | |
function uncompressed:write(data) | |
self[#self+1] = data | |
return true | |
end | |
if string.byte(input_path, -1) ~= 0x2f then -- '/' | |
input_path = input_path.."/" | |
end | |
local function traverse(path) | |
local children = posix_dirent.dir(input_path..path) | |
table.sort(children) | |
for child_index = 1, #children do | |
local child = children[child_index] | |
if posix_sys_stat.S_ISREG(assert(posix_sys_stat.stat(input_path..path..child)).st_mode) ~= 0 then | |
write_c_string(uncompressed, path..child) | |
local file = assert(io.open(input_path..path..child, "rb")) | |
local data = assert(file:read("*a")) | |
file:close() | |
local data_length = #data | |
if data_length < 0xff then | |
write_u8(uncompressed, data_length) | |
else | |
write_u8(uncompressed, 0xff) | |
write_u32_le(uncompressed, data_length) | |
end | |
uncompressed:write(data) | |
elseif child ~= "." and child ~= ".." then | |
write_c_string(uncompressed, path..child.."/") | |
traverse(path..child.."/") | |
end | |
end | |
end | |
traverse("") | |
uncompressed = table.concat(uncompressed) | |
local compressed_size = 1e6 -- 1 MB | |
local compressed = ffi.new("uint8_t[?]", compressed_size) | |
local compressed_length = C.LZ4_compress_default(uncompressed, compressed, #uncompressed, compressed_size) | |
printf("compressed_length: %s\n", compressed_length) | |
assert(compressed_length >= 0) | |
compressed = ffi.string(compressed, compressed_length) | |
assert(output_file:write("p64")) | |
write_u8(output_file, 2) -- unknown | |
write_u32_le(output_file, compressed_length) | |
assert(output_file:write(compressed)) | |
output_file:close() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env luajit | |
local argparse = require("argparse")(nil, [[Extract a Picotron .p64.rom. | |
Dependencies: | |
sudo apt install liblz4-1 luajit luarocks | |
luarocks install luaposix --local]]) | |
argparse:argument("input_path", "Path to .p64.rom.") | |
argparse:argument("output_path", "Directory to save files to.") | |
argparse:flag("--dry", "Don't create any files or directories.") | |
local arguments = argparse:parse() | |
local input_path = arguments.input_path | |
local input_file = assert(io.open(input_path, "rb")) | |
local output_path = arguments.output_path | |
local dry = arguments.dry | |
local bit = require("bit") | |
local ffi = require("ffi") | |
local posix = require("posix") | |
local printf_file = io.stdout | |
if printf_file == output_file then | |
printf_file = io.stderr | |
end | |
local function printf(...) | |
return assert(printf_file:write(string.format(...))) | |
end | |
local function unpack_u32_le(data) | |
local byte_4, byte_3, byte_2, byte_1 = string.byte(data, 1, 4) | |
return bit.bor( | |
bit.lshift(byte_1, 24), | |
bit.lshift(byte_2, 16), | |
bit.lshift(byte_3, 8), | |
byte_4 | |
) | |
end | |
local function file_read(file, length) | |
local data = assert(file:read(length)) | |
return data | |
end | |
local function file_read_u8(file) | |
return string.byte(file_read(file, 1)) | |
end | |
local function file_read_u32_le(file) | |
return unpack_u32_le(file_read(file, 4)) | |
end | |
local function file_read_c_string(file) | |
local data = {} | |
local data_index = 1 | |
while true do | |
local byte = file_read(file, 1) | |
if byte == "\x00" then | |
break | |
end | |
data[data_index] = byte | |
data_index = data_index+1 | |
end | |
return table.concat(data) | |
end | |
local function stream_string_new(data) | |
return { | |
data = data, | |
offset = 1, | |
} | |
end | |
local function stream_string_read(stream_string, length) | |
local data = stream_string.data | |
local start = stream_string.offset | |
local stop = start+length-1 | |
assert(stop <= #data) | |
stream_string.offset = stop+1 | |
return string.sub(data, start, stop) | |
end | |
local function stream_string_read_u8(stream_string) | |
return string.byte(stream_string_read(stream_string, 1)) | |
end | |
local function stream_string_read_u32_le(stream_string) | |
return unpack_u32_le(stream_string_read(stream_string, 4)) | |
end | |
local function stream_string_read_c_string(stream_string) | |
local data = stream_string.data | |
local start = stream_string.offset | |
local stop = string.find(data, "\x00", start, true) | |
assert(stop ~= nil) | |
stream_string.offset = stop+1 | |
stop = stop-1 | |
return string.sub(data, start, stop) | |
end | |
ffi.cdef([[ | |
const char *LZ4_versionString(void); | |
int LZ4_decompress_safe(const char *, char *, int, int); | |
]]) | |
local C = ffi.load("lz4") | |
printf("ffi.string(C.LZ4_versionString()): %q\n", ffi.string(C.LZ4_versionString())) | |
local magic = file_read(input_file, 3) | |
printf("magic: %q\n", magic) | |
assert(magic == "p64") | |
local unknown = file_read_u8(input_file) | |
printf("unknown: 0x%02x\n", unknown) | |
local compressed_length = file_read_u32_le(input_file) | |
printf("compressed_length: %s\n", compressed_length) | |
local decompressed_size = 1e6 -- 1 MB | |
local decompressed = ffi.new("uint8_t[?]", decompressed_size) | |
local compressed = file_read(input_file, compressed_length) | |
local actual_offset = input_file:seek() | |
printf("actual_offset: 0x%x\n", actual_offset) | |
local expected_offset = input_file:seek("end") | |
printf("expected_offset: 0x%x\n", expected_offset) | |
assert(actual_offset == expected_offset) | |
input_file:close() | |
local decompressed_length = C.LZ4_decompress_safe(compressed, decompressed, compressed_length, decompressed_size) | |
printf("decompressed_length: %s\n", decompressed_length) | |
assert(decompressed_length >= 0) | |
decompressed = ffi.string(decompressed, decompressed_length) | |
local stop_offset = #decompressed+1 | |
decompressed = stream_string_new(decompressed) | |
if string.byte(output_path, -1) ~= 0x2f then -- '/' | |
output_path = output_path.."/" | |
end | |
while decompressed.offset ~= stop_offset do | |
local path = stream_string_read_c_string(decompressed) | |
printf("path: %q\n", path) | |
assert(#path < 400, "\"filename parsing failed\"") | |
assert(string.find(path, "..", 1, true) == nil, "\"bad file description\"") | |
if string.byte(path, -1) == 0x2f then -- '/' | |
local path = output_path..path | |
if dry then | |
printf("would have created directory %q\n", path) | |
else | |
assert(posix.mkdir(path)) | |
end | |
else | |
local data_length = stream_string_read_u8(decompressed) | |
printf("data_length: %s\n", data_length) | |
if data_length == 0xff then | |
data_length = stream_string_read_u32_le(decompressed) | |
printf("data_length: %s\n", data_length) | |
end | |
local data = stream_string_read(decompressed, data_length) | |
local path = output_path..path | |
if dry then | |
printf("would have created file %q\n", path) | |
else | |
local output_file = assert(io.open(path, "w+b")) | |
assert(output_file:write(data)) | |
output_file:close() | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment