Created
December 4, 2025 20:23
-
-
Save skull-squadron/8b1111167de9b0120e2cf1d309014550 to your computer and use it in GitHub Desktop.
Convert pc2js JSON disk image to local image and extract files
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 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