Skip to content

Instantly share code, notes, and snippets.

@skull-squadron
Created December 4, 2025 20:23
Show Gist options
  • Select an option

  • Save skull-squadron/8b1111167de9b0120e2cf1d309014550 to your computer and use it in GitHub Desktop.

Select an option

Save skull-squadron/8b1111167de9b0120e2cf1d309014550 to your computer and use it in GitHub Desktop.
Convert pc2js JSON disk image to local image and extract files
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# Converts https://www.pcjs.org json disk image to real disk .img and extract files
#
# Usage: json2img [options..] {infile.json|https://path/to/web/disk.json} [OUT_DIR]
#
# -v|--verbose verbose
# -n|--no-op no-op
#
#
# If out_dir is not specified, it will be created based on the input file name.
#
#
# Example:
#
# json2img https://diskettes.pcjs.org/pcx86/dev/rom/ibm/5170/IBM-BIOS-SRC-5170V3.json
require 'json'
require 'fileutils'
require 'open-uri'
require 'digest/md5'
def warn_verbose(*msg)
warn(*msg) if VERBOSE
end
def write_file(fname, data, noop: false, verbose: false)
if noop
warn_verbose "Would write file #{fname} (#{data.bytesize} byte(s))"
return
end
if File.exist?(fname)
if File.binread(fname) == data
warn_verbose "File already exists and the same: #{fname}"
else
warn "File already exists and differs (skipping): #{fname}"
end
return
end
FileUtils.mkdir_p(File.dirname(fname), noop: noop, verbose: verbose)
warn_verbose "Writing file #{fname} (#{data.bytesize} byte(s))"
File.binwrite(fname, data)
end
NOOP = !ENV['NOOP'].nil? | ARGV.delete('-n') | ARGV.delete('--no-op')
VERBOSE = !ENV['VERBOSE'].nil? | ARGV.delete('-v') | ARGV.delete('--verbose')
in_file, out_dir = ARGV
if in_file.nil? || ARGV.delete('-h') || ARGV.delete('--help')
warn "Usage: #{File.basename($PROGRAM_NAME)} [options..] {infile.json|https://path/to/web/disk.json} [OUT_DIR]"
warn ''
warn ' -v|--verbose verbose'
warn ' -n|--no-op no-op'
warn ''
warn ''
warn ' If out_dir is not specified, it will be created based on the input file name.'
warn ''
warn ''
exit 1
end
in_file_prefix = File.basename(in_file, '.json')
out_dir ||= in_file_prefix
if in_file =~ /\A[a-zA-Z](?:[a-zA-Z0-9+.-])*:\/\/.+/
warn_verbose "Downloading remote file #{in_file}"
in_file = URI.open(in_file)
end
j = JSON.parse(in_file.read)
size = j['imageInfo']['diskSize']
cylinders = j['imageInfo']['cylinders']
heads = j['imageInfo']['heads']
sectors_per_track = j['imageInfo']['trackDefault']
bytes_per_sector = j['imageInfo']['sectorDefault']
unless size == cylinders * heads * sectors_per_track * bytes_per_sector
raise 'C * H * S * bytes_per_sector != size'
end
disk_img = "\0".b * size
files = j['fileTable'][1..].map do |f|
{
'filename' => f['path'].sub(/\A\//, ''),
'size' => f['size'],
'data' => "\0".b * f['size'],
}
end
j['diskData'].each do |pieces|
pieces.flatten.each do |sec|
c, h, s, l = sec['c'], sec['h'], (sec['s'] - 1), sec['l']
raise 'Bad individual sector != sector size' unless l == bytes_per_sector
# decode sector
sector = sec['d'].pack('l<*')
next if sector.bytes.all?(&:zero?)
pad_sz = bytes_per_sector - sector.bytesize
sector.append_as_bytes(sector[-4..-1] * (pad_sz/4)) unless pad_sz.zero?
raise 'sector incorrect size' unless sector.bytesize == bytes_per_sector
# write sector
img_off = ((c * heads + h) * sectors_per_track + s) * bytes_per_sector
img_end = img_off + bytes_per_sector - 1
disk_img[img_off..img_end] = sector
# write file parts
f_off = sec['o']
next if f_off.nil?
f = files[sec['f'] - 1]
f_sz = f['size']
raise "f_off #{f_off} > f_sz #{f_sz}" if f_off > f_sz
f_end = f_off + bytes_per_sector
overrun = f_end - f_sz
bin_end = bytes_per_sector - 1
if overrun > 0
bin_end -= overrun
f_end = f_sz
end
f['data'][f_off..f_end] = sector[0..bin_end]
end
end
# check file integrity
mismatch = false
j['fileTable'].each_with_index do |f, idx|
expected_size = f['size']
next unless expected_size
data = files[idx-1]['data']
actual_size = data.bytesize
expected_md5 = f['hash']
actual_md5 = Digest::MD5.hexdigest(data)
if expected_size == actual_size && expected_md5 == actual_md5
warn_verbose "file OK: #{f['path']} (size: #{size} byte(s), md5 #{actual_md5})"
else
warn "file mismatch: #{f['path']} (expected_md5: #{expected_md5}, actual_md5: #{actual_md5}; expected_size: #{expected_size}, actual_size: #{actual_size})"
mismatch = true
end
end
exit 1 if mismatch
# write files
root_dir = j['fileTable'][0]['path']&.sub(/\A\//, '') || in_file_prefix
write_file("#{out_dir}/#{in_file_prefix}.img", disk_img, noop: NOOP, verbose: VERBOSE)
files.each do |f|
filename = "#{out_dir}/#{root_dir}/#{f['filename']}"
data = f['data']
write_file(filename, data, noop: NOOP, verbose: VERBOSE)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment