Created
December 4, 2025 20:23
-
-
Save skull-squadron/8b1111167de9b0120e2cf1d309014550 to your computer and use it in GitHub Desktop.
Revisions
-
skull-squadron created this gist
Dec 4, 2025 .There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,169 @@ #!/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