Module: SafeImage::SvgMetadata
- Defined in:
- lib/safe_image/svg_metadata.rb
Constant Summary collapse
- MAX_SVG_BYTES =
1 * 1024 * 1024
- MAX_SVG_DEPTH =
64- MAX_SVG_ELEMENTS =
10_000- MAX_SVG_ATTRIBUTES =
50_000- MAX_SVG_DIMENSION =
100_000- MAX_SVG_PIXELS =
100_000_000- LENGTH_PATTERN =
/\A\s*([+]?(?:\d+(?:\.\d+)?|\.\d+))(?:px)?\s*\z/i.freeze
- VIEWBOX_SPLIT =
/[\s,]+/.freeze
Class Method Summary collapse
- .dimensions(path, max_pixels: nil, max_bytes: MAX_SVG_BYTES) ⇒ Object
- .parse(path, max_bytes: MAX_SVG_BYTES) ⇒ Object
- .parse_length(value) ⇒ Object
- .parse_view_box(value) ⇒ Object
- .probe(path, max_pixels: nil, max_bytes: MAX_SVG_BYTES) ⇒ Object
- .reject_unsafe_xml!(xml) ⇒ Object
- .safe_svg_path(path) ⇒ Object
- .validate_dimensions!(width, height, max_pixels: nil) ⇒ Object
- .validate_element!(element, depth:, counters:) ⇒ Object
- .validate_tree!(root) ⇒ Object
Class Method Details
.dimensions(path, max_pixels: nil, max_bytes: MAX_SVG_BYTES) ⇒ Object
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
# File 'lib/safe_image/svg_metadata.rb', line 33 def dimensions(path, max_pixels: nil, max_bytes: MAX_SVG_BYTES) path = safe_svg_path(path) doc = parse(path, max_bytes: max_bytes) root = doc.root width = parse_length(root.attributes["width"]) height = parse_length(root.attributes["height"]) unless width && height view_box = parse_view_box(root.attributes["viewBox"]) width ||= view_box&.fetch(2) height ||= view_box&.fetch(3) end validate_dimensions!(width, height, max_pixels: max_pixels) end |
.parse(path, max_bytes: MAX_SVG_BYTES) ⇒ Object
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
# File 'lib/safe_image/svg_metadata.rb', line 49 def parse(path, max_bytes: MAX_SVG_BYTES) path = safe_svg_path(path) size = File.size(path) raise LimitError, "SVG exceeds #{max_bytes} bytes" if size > max_bytes xml = File.binread(path, max_bytes + 1) raise LimitError, "SVG exceeds #{max_bytes} bytes" if xml.bytesize > max_bytes reject_unsafe_xml!(xml) doc = REXML::Document.new(xml) raise InvalidImageError, "SVG root required" unless doc.root&.name == "svg" validate_tree!(doc.root) doc rescue REXML::ParseException => e raise InvalidImageError, "invalid SVG: #{e.}" end |
.parse_length(value) ⇒ Object
77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/safe_image/svg_metadata.rb', line 77 def parse_length(value) value = value.to_s match = LENGTH_PATTERN.match(value) return nil unless match number = Float(match[1]) return nil unless number.finite? && number.positive? number rescue ArgumentError nil end |
.parse_view_box(value) ⇒ Object
90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/safe_image/svg_metadata.rb', line 90 def parse_view_box(value) parts = value.to_s.strip.split(VIEWBOX_SPLIT) return nil unless parts.length == 4 numbers = parts.map { |part| Float(part) } return nil unless numbers.all?(&:finite?) && numbers[2].positive? && numbers[3].positive? numbers rescue ArgumentError nil end |
.probe(path, max_pixels: nil, max_bytes: MAX_SVG_BYTES) ⇒ Object
20 21 22 23 24 25 26 27 28 29 30 31 |
# File 'lib/safe_image/svg_metadata.rb', line 20 def probe(path, max_pixels: nil, max_bytes: MAX_SVG_BYTES) started = Process.clock_gettime(Process::CLOCK_MONOTONIC) path = safe_svg_path(path) width, height = dimensions(path, max_pixels: max_pixels, max_bytes: max_bytes) { input_format: "svg", width: width, height: height, frames: 1, duration_ms: (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 } end |
.reject_unsafe_xml!(xml) ⇒ Object
72 73 74 75 |
# File 'lib/safe_image/svg_metadata.rb', line 72 def reject_unsafe_xml!(xml) raise InvalidImageError, "doctype is not allowed in SVG" if xml.match?(/<!DOCTYPE/i) raise InvalidImageError, "XML processing instructions are not allowed in SVG" if xml.match?(/<\?(?!xml\s)/i) end |
.safe_svg_path(path) ⇒ Object
66 67 68 69 70 |
# File 'lib/safe_image/svg_metadata.rb', line 66 def safe_svg_path(path) path = PathSafety.ensure_regular_file!(path) raise UnsupportedFormatError, "not an SVG file: #{path}" unless File.extname(path.to_s).downcase == ".svg" path.to_s end |
.validate_dimensions!(width, height, max_pixels: nil) ⇒ Object
102 103 104 105 106 107 108 109 110 111 |
# File 'lib/safe_image/svg_metadata.rb', line 102 def validate_dimensions!(width, height, max_pixels: nil) raise InvalidImageError, "SVG dimensions are missing or invalid" unless width&.positive? && height&.positive? raise LimitError, "SVG dimensions exceed #{MAX_SVG_DIMENSION}px" if width > MAX_SVG_DIMENSION || height > MAX_SVG_DIMENSION pixels = width * height limit = max_pixels || MAX_SVG_PIXELS raise LimitError, "SVG has #{pixels.to_i} pixels, exceeds #{limit}" if pixels > limit [width.ceil, height.ceil] end |
.validate_element!(element, depth:, counters:) ⇒ Object
118 119 120 121 122 123 124 125 126 127 128 129 130 |
# File 'lib/safe_image/svg_metadata.rb', line 118 def validate_element!(element, depth:, counters:) raise LimitError, "SVG nesting exceeds #{MAX_SVG_DEPTH}" if depth > MAX_SVG_DEPTH counters[:elements] += 1 raise LimitError, "SVG has too many elements" if counters[:elements] > MAX_SVG_ELEMENTS counters[:attributes] += element.attributes.length raise LimitError, "SVG has too many attributes" if counters[:attributes] > MAX_SVG_ATTRIBUTES element.elements.each do |child| validate_element!(child, depth: depth + 1, counters: counters) end end |