class OBF::Validator
Public Class Methods
validate_file(path)
click to toggle source
# File lib/obf/validator.rb, line 38 def self.validate_file(path) type = Utils::identify_file(path) fn = File.basename(path) filesize = File.size(path) if type == :obf validate_obf(path) elsif type == :obz validate_obz(path) else hint = res = { :filename => fn, :filesize => filesize, :valid => false, :errors => 1, :results => [{ 'type' => 'valid_file', 'description' => 'valid .obf or .obz file', 'valid' => false, 'error' => 'file must be a single .obf JSON file or a .obz zip package' }] } if type == :json_not_obf res[:results] << { 'type' => 'json_parse', 'description' => 'valid JSON object', 'valid' => false, 'error' => 'file contains a JSON object but it does not appear to be an OBF-formatted object' } elsif type == :json_not_object res[:results] << { 'type' => 'json_parse', 'description' => 'valid JSON object', 'valid' => false, 'error' => 'file contains valid JSON, but a type other than Object. OBF files do not support arrays, strings, etc. as the root object' } end res end end
validate_obf(path, opts={})
click to toggle source
# File lib/obf/validator.rb, line 79 def self.validate_obf(path, opts={}) v = self.new fn = fn filesize = 0 content = nil if opts['zipper'] fn = path content = opts['zipper'].read(path) filesize = opts['zipper'].glob(path)[0].size else fn = File.basename(path) content = File.read(path) filesize = File.size(path) end results = v.validate_obf(content, fn, opts) { :filename => fn, :filesize => filesize, :valid => v.errors == 0, :errors => v.errors, :warnings => v.warnings, :results => results } end
validate_obz(path)
click to toggle source
# File lib/obf/validator.rb, line 104 def self.validate_obz(path) v = self.new fn = File.basename(path) results, sub_results = v.validate_obz(path, fn) errors = ([v.errors] + sub_results.map{|r| r[:errors] }).inject(:+) warnings = ([v.warnings] + sub_results.map{|r| r[:warnings] }).inject(:+) { :filename => fn, :filesize => File.size(path), :valid => errors == 0, :errors => errors, :warnings => warnings, :results => results, :sub_results => sub_results } end
Public Instance Methods
add_check(type, description, &block)
click to toggle source
# File lib/obf/validator.rb, line 3 def add_check(type, description, &block) return if @blocked @checks << { 'type' => type, 'description' => description, 'valid' => true } begin block.call rescue ValidationError => e @errors += 1 @checks[-1]['valid'] = false @checks[-1]['error'] = e.message @blocked = true if e.blocker end end
err(message, blocker=false)
click to toggle source
# File lib/obf/validator.rb, line 20 def err(message, blocker=false) raise ValidationError.new(blocker), message end
errors()
click to toggle source
# File lib/obf/validator.rb, line 30 def errors @errors || 0 end
setup()
click to toggle source
# File lib/obf/validator.rb, line 121 def setup @blocked = nil @errored = false @warnings = 0 @errors = 0 @checks = [] @sub_checks = [] end
validate_obf(content, filename, opts={})
click to toggle source
# File lib/obf/validator.rb, line 281 def validate_obf(content, filename, opts={}) setup # TODO enforce extra attributes being defined with ext_ add_check('filename', "file name") do if !filename.match(/\.obf$/) warn "filename should end with .obf" end end json = nil add_check('valid_json', "JSON file") do begin json = JSON.parse(content) rescue => e err "Couldn't parse as JSON", true end end ext = nil add_check('to_external', "OBF structure") do begin External.from_obf(json, {}) ext = JSON.parse(content) rescue External::StructureError => e err "Couldn't parse structure: #{e.message}", true end end add_check('format_version', "format version") do if !ext['format'] err "format attribute is required, set to #{OBF::FORMAT}" end version = ext['format'].split(/-/, 3)[-1].to_f if version > OBF::FORMAT_CURRENT_VERSION err "format version (#{version}) is invalid, current version is #{OBF::FORMAT_CURRENT_VERSION}" elsif version < OBF::FORMAT_CURRENT_VERSION warn "format version (#{version}) is old, consider updating to #{OBF::FORMAT_CURRENT_VERSION}" end end add_check('id', "board ID") do if !ext['id'] err "id attribute is required" end end add_check('locale', "locale") do if !ext['locale'] err "locale attribute is required, please set to \"en\" for English" end end add_check('extras', "extra attributes") do attrs = ['format', 'id', 'locale', 'url', 'data_url', 'name', 'description_html', 'default_layout', 'buttons', 'images', 'sounds', 'grid', 'license'] ext.keys.each do |key| if !attrs.include?(key) && !key.match(/^ext_/) warn "#{key} attribute is not defined in the spec, should be prefixed with ext_yourapp_" end end end add_check('description', "descriptive attributes") do if !ext['name'] warn "name attribute is strongly recommended" end if !ext['description_html'] warn "description_html attribute is recommended" end end add_check('background', "background attribute") do if ext['background'] && !ext['background'].is_a?(Hash) err "background attributee must be a hash" end end add_check('buttons', "buttons attribute") do if !ext['buttons'] err "buttons attribute is required" elsif !ext['buttons'].is_a?(Array) err "buttons attribute must be an array" end end add_check('grid', "grid attribute") do if !ext['grid'] err "grid attribute is required" elsif !ext['grid'].is_a?(Hash) err "grid attribute must be a hash" elsif !ext['grid']['rows'].is_a?(Fixnum) || ext['grid']['rows'] < 1 err "grid.row attribute must be a valid positive number" elsif !ext['grid']['columns'].is_a?(Fixnum) || ext['grid']['columns'] < 1 err "grid.column attribute must be a valid positive number" end if ext['grid']['rows'] > 20 warn "grid.row (#{ext['grid']['rows']}) is probably too large a number for most systems" end if ext['grid']['columns'] > 20 warn "grid.column (#{ext['grid']['columns']}) is probably too large a number for most systems" end if !ext['grid']['order'] err "grid.order is required" elsif !ext['grid']['order'].is_a?(Array) err "grid.order must be an array of arrays" elsif ext['grid']['order'].length != ext['grid']['rows'] err "grid.order length (#{ext['grid']['order'].length}) must match grid.rows (#{ext['grid']['rows']})" elsif !ext['grid']['order'].all?{|r| r.is_a?(Array) && r.length == ext['grid']['columns'] } err "grid.order must contain #{ext['grid']['rows']} arrays each of size #{ext['grid']['columns']}" end attrs = ['rows', 'columns', 'order'] ext['grid'].keys.each do |key| if !attrs.include?(key) && !key.match(/^ext_/) warn "grid.#{key} attribute is not defined in the spec, should be prefixed with ext_yourapp_" end end end add_check('grid_ids', "button IDs in grid.order attribute") do button_ids = [] if ext['buttons'] && ext['buttons'].is_a?(Array) ext['buttons'].each{|b| button_ids << b['id'] if b.is_a?(Hash) && b['id'] } end used_button_ids = [] if ext['grid'] && ext['grid']['order'] && ext['grid']['order'].is_a?(Array) ext['grid']['order'].each do |row| if row.is_a?(Array) row.each do |id| if id used_button_ids << id if !button_ids.include?(id) err "grid.order references button with id #{id} but no button with that id found in buttons attribute" end end end end end end warn("board has no buttons defined in the grid") if used_button_ids.length == 0 warn("not all defined buttons were included in the grid order (#{(button_ids - used_button_ids).join(',')})") if (button_ids - used_button_ids).length > 0 end button_image_ids = [] if ext && ext['buttons'] && ext['buttons'].is_a?(Array) ext['buttons'].each{|b| button_image_ids << b['image_id'] if b.is_a?(Hash) && b['image_id'] } end add_check('images', "images attribute") do if !ext['images'] err "images attribute is required" elsif !ext['images'].is_a?(Array) err "images attribute must be an array" end end if ext && ext['images'] && ext['images'].is_a?(Array) ext['images'].each_with_index do |image, idx| add_check("image[#{idx}]", "image at images[#{idx}]") do if !image.is_a?(Hash) err "image must be a hash" elsif !image['id'] err "image.id is required" elsif !image['width'] || !image['width'].is_a?(Fixnum) err "image.width must be a valid positive number" elsif !image['height'] || !image['height'].is_a?(Fixnum) err "image.height must be a valid positive number" elsif !image['content_type'] || !image['content_type'].match(/^image\/.+$/) err "image.content_type must be a valid image mime type" elsif !image['url'] && !image['data'] && !image['symbol'] && !image['path'] err "image must have data, url, path or symbol attribute defined" elsif image['data'] && !image['data'].match(/^data:image\/.+;base64,.+$/) err "image.data must be a valid data URI if defined" elsif image['symbol'] && !image['symbol'].is_a?(Hash) err "image.symbol must be a hash if defined" end if image['path'] if opts['zipper'] if opts['zipper'].glob(image['path']).length == 0 err "image.path \"#{image['path']}\" not found in .obz package" end else err "image.path should not be defined outside of an .obz package" end end attrs = ['id', 'width', 'height', 'content_type', 'data', 'url', 'symbol', 'path', 'data_url', 'license'] image.keys.each do |key| if !attrs.include?(key) && !key.match(/^ext_/) warn "image.#{key} attribute is not defined in the spec, should be prefixed with ext_yourapp_" end end end end end add_check('sounds', "sounds attribute") do if !ext['sounds'] err "sounds attribute is required" elsif !ext['sounds'].is_a?(Array) err "sounds attribute must be an array" end end if ext && ext['sounds'] && ext['sounds'].is_a?(Array) ext['sounds'].each_with_index do |sound, idx| add_check("sounds[#{idx}]", "sound at sounds[#{idx}]") do if !sound.is_a?(Hash) err "sound must be a hash" elsif !sound['id'] err "sound.id is required" elsif !sound['duration'] || !sound['duration'].is_a?(Fixnum) err "sound.duration must be a valid positive number" elsif !sound['content_type'] || !sound['content_type'].match(/^audio\/.+$/) err "sound.content_type must be a valid audio mime type" elsif !sound['url'] && !sound['data'] && !sound['symbol'] && !sound['path'] err "sound must have data, url, path or symbol attribute defined" elsif sound['data'] && !sound['data'].match(/^data:audio\/.+;base64,.+$/) err "sound.data must be a valid data URI if defined" end attrs = ['id', 'duration', 'content_type', 'data', 'url', 'path', 'data_url', 'license'] sound.keys.each do |key| if !attrs.include?(key) && !key.match(/^ext_/) warn "sound.#{key} attribute is not defined in the spec, should be prefixed with ext_yourapp_" end end end end end if ext && ext['buttons'] && ext['buttons'].is_a?(Array) ext['buttons'].each_with_index do |button, idx| add_check("buttons[#{idx}]", "button at buttons[#{idx}]") do if !button.is_a?(Hash) err "button must be a hash" elsif !button['id'] err "button.id is required" elsif !button['label'] err "button.label is required" end ['top', 'left', 'width', 'height'].each do |attr| if button[attr] && ((!button[attr].is_a?(Fixnum) && !button[attr].is_a?(Float)) || button[attr] < 0) warn "button.#{attr} should be a positive number" end end ['background_color', 'border_color'].each do |color| if button[color] if !button[color].match(/^\s*rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*(,\s*[01]\.?\d*)?\)\s*/) err "button.#{color} must be a valid rgb or rgba value if defined (\"#{button[color]}\" is invalid)" end end end if button['hidden'] != nil && button['hidden'] != true && button['hidden'] != false err "button.hidden must be a boolean if defined" end if !button['image_id'] warn "button.image_id is recommended" end if button['action'] && !button['action'].match(/^(:|\+)/) err "button.action must start with either : or + if defined" end if button['action'] && !button['action'].is_a?(Array) err "button.actions must be an array of strings" end attrs = ['id', 'label', 'vocalization', 'image_id', 'sound_id', 'hidden', 'background_color', 'border_color', 'action', 'actions', 'load_board', 'top', 'left', 'width', 'height'] button.keys.each do |key| if !attrs.include?(key) && !key.match(/^ext_/) warn "button.#{key} attribute is not defined in the spec, should be prefixed with ext_yourapp_" end end if button['load_board'] && button['load_board']['path'] if !opts['zipper'] err "button.load_board.path is set but this isn't a zipped file" else json = JSON.parse(opts['zipper'].read(button['load_board']['path'])) rescue nil if !json || !json['id'] err "button.load_board.path references #{button['load_board']['path']} which isn't found in the zipped file" end end end end end end return @checks end
validate_obz(path, filename)
click to toggle source
# File lib/obf/validator.rb, line 130 def validate_obz(path, filename) setup add_check('filename', "file name") do if !filename.match(/\.obz$/) warn "filename should end with .obz" end end valid_zip = false add_check('zip', 'valid zip') do begin Utils::load_zip(path) do |zipper| end valid_zip = true rescue Zip::Error => e err "file is not a valid zip package" end end if valid_zip Utils::load_zip(path) do |zipper| json = nil add_check('manifest', 'manifest.json') do if zipper.glob('manifest.json').length == 0 err "manifest.json is required in the zip package" end json = JSON.parse(zipper.read('manifest.json')) rescue nil if !json err "manifest.json must contain a valid JSON structure" end end if json add_check('manifest_format', 'manifest.json format version') do if !json['format'] err "format attribute is required, set to #{OBF::FORMAT}" end version = json['format'].split(/-/, 3)[-1].to_f if version > OBF::FORMAT_CURRENT_VERSION err "format version (#{version}) is invalid, current version is #{OBF::FORMAT_CURRENT_VERSION}" elsif version < OBF::FORMAT_CURRENT_VERSION warn "format version (#{version}) is old, consider updating to #{OBF::FORMAT_CURRENT_VERSION}" end end add_check('manifest_root', 'manifest.json root attribute') do if !json['root'] err "root attribute is required" end if zipper.glob(json['root']).length == 0 err "root attribute must reference a file in the package" end end paths = nil add_check('manifest_paths', 'manifest.json paths attribute') do if !json['paths'] || !json['paths'].is_a?(Hash) err "paths attribute must be a valid hash" end if !json['paths']['boards'] || !json['paths']['boards'].is_a?(Hash) err "paths.boards must be a valid hash" end if json['paths']['images'] && !json['paths']['images'].is_a?(Hash) err "paths.images must be a valid hash if defined" end if json['paths']['sounds'] && !json['paths']['sounds'].is_a?(Hash) err "paths.sounds must be a valid hash if defined" end end add_check('manifest_extras', 'manifest.json extra attributes') do attrs = ['format', 'root', 'paths'] json.keys.each do |key| if !attrs.include?(key) && !key.match(/^ext_/) warn "#{key} attribute is not defined in the spec, should be prefixed with ext_yourapp_" end end attrs = ['boards', 'images', 'sounds'] json['paths'].keys.each do |key| if !attrs.include?(key) && !key.match(/^ext_/) warn "paths.#{key} attribute is not defined in the spec, should be prefixed with ext_yourapp_" end end end found_paths = ['manifest.json'] if json['paths'] && json['paths']['boards'] && json['paths']['boards'].is_a?(Hash) json['paths']['boards'].each do |id, path| add_check("manifest_boards[#{id}]", "manifest.json path.boards.#{id}") do found_paths << path if !zipper.glob(path) err "board path (#{path}) not found in the zip package" end board_json = JSON.parse(zipper.read(path)) rescue nil if !board_json || board_json['id'] != id board_id = (board_json && board_json['id']) || "null" err "board at path (#{path}) defined in manifest with id \"#{id}\" but actually has id \"#{board_id}\"" end end end end if json['paths'] && json['paths']['images'] && json['paths']['images'].is_a?(Hash) json['paths']['images'].each do |id, path| add_check("manifest_images[#{id}]", "manifest.json path.images.#{id}") do found_paths << path if !zipper.glob(path) err "image path (#{path}) not found in the zip package" end end end end if json['paths'] && json['paths']['sounds'] && json['paths']['sounds'].is_a?(Hash) json['paths']['sounds'].each do |id, path| add_check("manifest_sounds[#{id}]", "manifest.json path.sounds.#{id}") do found_paths << path if !zipper.glob(path) err "sound path (#{path}) not found in the zip package" end end end end actual_paths = zipper.all_files add_check('extra_paths', 'manifest.json extra paths') do (actual_paths - found_paths).each do |path| warn "the file \"#{path}\" isn't listed in manifest.json" end end sub_results = [] if json['paths'] && json['paths']['boards'] && json['paths']['boards'].is_a?(Hash) json['paths']['boards'].each do |key, path| sub = Validator.validate_obf(path, {'zipper' => zipper}) sub_results << sub end end @sub_checks = sub_results end if zipper.glob('manifest.json').length > 0 json = JSON.parse(zipper.read('manifest.json')) rescue nil if json['root'] && json['format'] && json['format'].match(/^open-board-/) type = :obz end end end end return [@checks, @sub_checks] end
warn(message)
click to toggle source
# File lib/obf/validator.rb, line 24 def warn(message) @warnings += 1 @checks[-1]['warnings'] ||= [] @checks[-1]['warnings'] << message end
warnings()
click to toggle source
# File lib/obf/validator.rb, line 34 def warnings @warnings || 0 end