Skip to content

Instantly share code, notes, and snippets.

@GreyCat
Created July 14, 2017 14:15
Show Gist options
  • Save GreyCat/f9e856b0bc9ac99ac2ba32a8f22f2c96 to your computer and use it in GitHub Desktop.
Save GreyCat/f9e856b0bc9ac99ac2ba32a8f22f2c96 to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby
require 'yaml'
require 'set'
class ErrorInPath < RuntimeError
def initialize(msg, path)
super("#{msg} in /#{path.join('/')}")
@msg = msg
@path = path
end
end
class OutputWriter
attr_reader :buf
def initialize
@buf = ''
@indent = 0
end
def inc
@indent += 1
end
def dec
@indent -= 1
end
def <<(s)
unless s.empty?
@buf << ' ' * @indent
@buf << s
end
@buf << "\n"
end
def out_lines(s)
lines = s.split(/\n/) # NB: no -1, to consume trailing newline
lines.each { |l| self << l }
end
end
class KSYPrettyPrinter
SCHEMA_META_TOP = [
[:scalar, true, 'id'],
[:scalar, false, '-orig-id'],
[:scalar, false, 'title'],
[:scalar, false, 'application'],
[:scalar, false, 'file-extension'],
[:map_generic, false, 'xref'],
[:scalar, true, 'license'],
[:scalar, false, 'ks-version'],
[:seq_generic, false, 'imports'], # FIXME type
]
SCHEMA_META_AUX = [
[:scalar, false, 'encoding'],
[:scalar, false, 'endian'], # FIXME type
]
SCHEMA_DOC = [
[:doc, false, 'doc'],
[:scalar, false, 'doc-ref'], # FIXME type
]
SCHEMA_ORIG_ID = [
[:scalar, false, '-orig-id'],
]
SCHEMA_ATTR = [
[:scalar, false, 'size'],
[:scalar, false, 'type'], # FIXME
[:scalar, false, 'enum'],
[:scalar, false, 'contents'],
[:scalar, false, 'terminator'],
[:scalar, false, 'include'],
[:scalar, false, 'consume'],
[:scalar, false, 'eos-error'],
[:scalar, false, 'encoding'],
[:scalar, false, 'process'],
[:scalar, false, 'repeat'],
[:scalar, false, 'repeat-eos'],
[:scalar, false, 'repeat-expr'],
[:scalar, false, 'repeat-until'],
[:scalar, false, 'if'],
[:doc, false, 'doc'],
[:scalar, false, 'doc-ref'], # FIXME
]
SCHEMA_INST = [
[:scalar, false, 'io'],
[:scalar, false, 'pos'],
[:scalar, false, 'value'],
]
def initialize(fn)
@ksy = YAML.parse(File.read(fn))
@out = OutputWriter.new
end
def dump
dump_type(@ksy.children[0], [])
@out.buf
end
def dump_type(node, path)
meta = find_01(node, 'meta', path)
if path.empty?
raise ErrorInPath.new('Top-level meta is mandatory', path + ['meta']) unless meta
meta_open
dump_meta_top(meta, path + ['meta'])
@out.dec
elsif not meta.nil?
meta_open
dump_meta_aux(meta, path + ['meta'])
@out.dec
end
dump_by_schema(node, SCHEMA_DOC, path, false)
seq = find_01(node, 'seq', path)
dump_seq(seq, path + ['seq']) unless seq.nil?
# The rest saves original order
each_map_element(node, ['meta', 'doc', 'doc-ref', 'seq'], path) { |k, v|
case k
when 'instances'
dump_map(v, k, path + [k]) { |el, path| dump_instance(el, path) }
when 'types'
dump_map(v, k, path + [k]) { |el, path| dump_type(el, path) }
when 'enums'
dump_map(v, k, path + [k]) { |el, path| dump_enum(el, path) }
else
raise ErrorInPath.new("Unexpected key #{k}", path + [k])
end
}
end
# ========================================================================
def meta_open
@out << 'meta:'
@out.inc
end
def dump_meta_top(meta, path)
dump_by_schema(meta, SCHEMA_META_TOP + SCHEMA_META_AUX, path)
# TODO: check that at least one of title/application/file-extension exists
end
def dump_meta_aux(meta, path)
dump_by_schema(meta, SCHEMA_META_AUX, path)
end
# ========================================================================
def dump_seq(seq, path)
@out << "seq:"
@out.inc
seq.children.each_with_index { |attr, i|
dump_attr(attr, path + [i])
}
@out.dec
end
def dump_attr(attr, path)
id = find_1(attr, 'id', path)
@out << "- id: #{id.value}"
@out.inc
dump_by_schema(attr, SCHEMA_ORIG_ID + SCHEMA_ATTR, path, true, Set.new(['id']))
@out.dec
end
# ========================================================================
def dump_instance(inst, path)
dump_by_schema(inst, SCHEMA_ORIG_ID + SCHEMA_INST + SCHEMA_ATTR, path)
end
# ========================================================================
def dump_enum(enum, path)
each_map_element(enum, [], path) { |k, v|
# TODO: check that v is scalar and handle fancy enums
@out << "#{k}: #{v.value}"
}
end
# ========================================================================
def dump_map(mapping, key, path)
@out << "#{key}:"
@out.inc
each_map_element(mapping, [], path) { |k, v|
@out << "#{k}:"
@out.inc
yield v, path
@out.dec
}
@out.dec
end
def dump_key_scalar(key, val)
v = val.value
if v =~ /\n/
@out << "#{key}: |"
@out.inc
@out.out_lines(v)
@out.dec
else
@out << "#{key}: #{val.value}"
end
end
def dump_key_map_generic(key, val, path)
@out << "#{key}:"
@out.inc
@out << "HERE BE DRAGONS!"
@out.dec
end
private
def dump_by_schema(mapping, schema, path, strict = true, extra_legal = Set.new)
schema.each { |rec|
t, must, k = rec
val = must ? find_1(mapping, k, path) : find_01(mapping, k, path)
next if val.nil?
case t
when :scalar, :doc
dump_key_scalar(k, val)
when :map_generic
dump_key_map_generic(k, val, path)
else
raise ErrorInPath.new("Unknown schema type #{t}", path + [k])
end
}
if strict
legal = Set.new(schema.map { |x| x[2] }) + extra_legal
each_map_element(mapping, legal, path) { |k, v|
raise ErrorInPath.new("Checking with schema, got unknown key #{k.inspect}, legal = #{legal.to_a.sort.join('/')}", path)
}
end
end
def find_01(mapping, key, path)
mapping.children.each_slice(2) { |k, v|
# TODO: check that k is scalar
return v if k.value == key
}
nil
end
def find_1(mapping, key, path)
r = find_01(mapping, key, path)
unless r.nil?
return r
else
raise ErrorInPath.new("Unable to find #{key.inspect}", path)
end
end
def each_map_element(mapping, except, path)
mapping.children.each_slice(2) { |k, v|
# TODO: check that k is scalar
next if except.include?(k.value)
yield k.value, v
}
end
end
ARGV.each { |fn| print KSYPrettyPrinter.new(fn).dump }
Copy link

ghost commented Sep 30, 2017

FYI: the "value"-method is missing in line 214, use "gif.ksy" from kaitai_struct_formats/image to reproduce message

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment