svg_conform 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitattributes +1 -0
- data/.github/workflows/rake.yml +3 -0
- data/Gemfile +7 -3
- data/Gemfile.lock +272 -0
- data/TODO.issue-100/00-overview.md +135 -0
- data/TODO.issue-100/01-build-lens-core-v1.md +92 -0
- data/TODO.issue-100/02-adopt-lens-core-in-svg.md +123 -0
- data/TODO.issue-100/03-ship-svg-1-1-full-static.md +138 -0
- data/TODO.issue-100/04-ship-svg-2-secure-static.md +131 -0
- data/TODO.issue-100/05-freeze-candidate-and-handoff.md +108 -0
- data/TODO.issue-100/90-progress.md +65 -0
- data/config/profiles/metanorma.yml +3 -3
- data/config/profiles/svg_1_1_full_static.yml +69 -0
- data/config/profiles/svg_2_secure_static.yml +55 -0
- data/config/requirements/processing_features_static.yml +29 -0
- data/data/svg_1_1/manifest.yml +7 -0
- data/data/svg_1_1/svg11-flat-20110816.dtd +5664 -0
- data/docs/api_reference.adoc +19 -1
- data/docs/cli_guide.adoc +35 -45
- data/docs/diagnostics.adoc +73 -0
- data/docs/lens_core_handoff.adoc +84 -0
- data/docs/profiles.adoc +94 -1
- data/lib/svg_conform/cli.rb +3 -3
- data/lib/svg_conform/commands/check.rb +11 -60
- data/lib/svg_conform/conformance_report.rb +79 -1
- data/lib/svg_conform/diagnostics/lens_core_adapter.rb +200 -0
- data/lib/svg_conform/document.rb +12 -1
- data/lib/svg_conform/node_helpers.rb +1 -1
- data/lib/svg_conform/profile.rb +11 -2
- data/lib/svg_conform/quality_metrics/complexity_calculator.rb +3 -3
- data/lib/svg_conform/references/base_reference.rb +153 -0
- data/lib/svg_conform/report_comparator.rb +4 -4
- data/lib/svg_conform/requirements/base_requirement.rb +6 -0
- data/lib/svg_conform/requirements/external_references_requirement.rb +234 -0
- data/lib/svg_conform/requirements/link_validation_requirement.rb +2 -0
- data/lib/svg_conform/requirements/processing_features_requirement.rb +175 -0
- data/lib/svg_conform/requirements/svg_1_1_document_requirement.rb +213 -0
- data/lib/svg_conform/requirements.rb +6 -0
- data/lib/svg_conform/sax_document.rb +8 -4
- data/lib/svg_conform/sax_validation_handler.rb +22 -1
- data/lib/svg_conform/validation/css2_core_grammar.rb +359 -0
- data/lib/svg_conform/validation/css_references.rb +122 -0
- data/lib/svg_conform/validation/error_tracker.rb +3 -1
- data/lib/svg_conform/validation/svg_1_1_document_conformance.rb +155 -0
- data/lib/svg_conform/validation_context.rb +25 -5
- data/lib/svg_conform/validation_result.rb +91 -16
- data/lib/svg_conform/version.rb +1 -1
- data/lib/svg_conform.rb +24 -0
- data/spec/fixtures/diagnostics/external-reference-info.svg +6 -0
- data/spec/fixtures/plantuml/README.adoc +11 -0
- data/spec/fixtures/plantuml/unit-type.svg +1 -0
- data/spec/fixtures/profiles/svg_1_1_full_static/invalid/animation.svg +4 -0
- data/spec/fixtures/profiles/svg_1_1_full_static/invalid/css-grammar.svg +4 -0
- data/spec/fixtures/profiles/svg_1_1_full_static/invalid/dtd-attribute.svg +4 -0
- data/spec/fixtures/profiles/svg_1_1_full_static/invalid/dtd-content-model.svg +4 -0
- data/spec/fixtures/profiles/svg_1_1_full_static/invalid/interaction.svg +4 -0
- data/spec/fixtures/profiles/svg_1_1_full_static/invalid/malformed-xml.svg +4 -0
- data/spec/fixtures/profiles/svg_1_1_full_static/invalid/script.svg +5 -0
- data/spec/fixtures/profiles/svg_1_1_full_static/invalid/wrong-namespace.svg +4 -0
- data/spec/fixtures/profiles/svg_1_1_full_static/invalid/wrong-version.svg +4 -0
- data/spec/fixtures/profiles/svg_1_1_full_static/invalid/xml-stylesheet.svg +5 -0
- data/spec/fixtures/profiles/svg_1_1_full_static/valid/css.svg +5 -0
- data/spec/fixtures/profiles/svg_1_1_full_static/valid/foreign-metadata.svg +6 -0
- data/spec/fixtures/profiles/svg_1_1_full_static/valid/minimal-full.svg +5 -0
- data/spec/fixtures/profiles/svg_1_1_full_static/valid/xml-stylesheet.svg +5 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/animation.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/css-import.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/css-url.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/data-bad-png.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/data-empty-png.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/data-malformed-base64.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/data-malformed-percent.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/data-unsupported-type.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/file-like-fragment.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/http.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/https.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/interaction.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/protocol-relative.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/relative-file-fragment.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/relative-file.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/script.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/svg-data-excessive-nesting.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/svg-data-malformed-xml.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/svg-data-nested-external.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/urn.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/invalid/xml-stylesheet.svg +5 -0
- data/spec/fixtures/profiles/svg_2_secure_static/valid/jpeg-base64.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/valid/jpeg-percent.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/valid/png-base64.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/valid/png-percent.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/valid/same-document-fragment.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/valid/svg-base64.svg +4 -0
- data/spec/fixtures/profiles/svg_2_secure_static/valid/svg-percent.svg +4 -0
- data/spec/svg_conform/commands/check_command_spec.rb +22 -2
- data/spec/svg_conform/commands/profiles_command_spec.rb +18 -0
- data/spec/svg_conform/conformance_report_spec.rb +90 -0
- data/spec/svg_conform/diagnostics/lens_core_adapter_spec.rb +136 -0
- data/spec/svg_conform/gemspec_spec.rb +21 -0
- data/spec/svg_conform/profiles/svg_1_1_full_static_profile_spec.rb +146 -0
- data/spec/svg_conform/profiles/svg_2_secure_static_profile_spec.rb +150 -0
- data/spec/svg_conform/profiles_plantuml_spec.rb +81 -0
- data/spec/svg_conform/references/data_uri_reference_spec.rb +97 -0
- data/spec/svg_conform/references/integration_spec.rb +30 -0
- data/spec/svg_conform/references/reference_classifier_spec.rb +17 -0
- data/spec/svg_conform/requirements/external_references_requirement_spec.rb +109 -0
- data/spec/svg_conform/requirements/link_validation_requirement_spec.rb +16 -0
- data/spec/svg_conform/requirements/processing_features_requirement_spec.rb +75 -0
- data/spec/svg_conform/requirements/svg_1_1_document_requirement_spec.rb +141 -0
- data/spec/svg_conform/validation/css2_core_grammar_spec.rb +116 -0
- data/spec/svg_conform/validation/svg_1_1_document_conformance_spec.rb +144 -0
- data/spec/svg_conform/validation_context_source_plumbing_spec.rb +122 -0
- data/spec/svg_conform/validation_context_spec.rb +39 -0
- data/spec/svg_conform/validation_result_spec.rb +123 -0
- data/svg_conform.gemspec +3 -2
- metadata +102 -6
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Path is fixed by the card; the cop expects svg11_ instead of svg_1_1_.
|
|
6
|
+
RSpec.describe SvgConform::Requirements::Svg11DocumentRequirement do # rubocop:disable RSpec/SpecFilePathFormat
|
|
7
|
+
let(:svg_ns) { 'xmlns="http://www.w3.org/2000/svg"' }
|
|
8
|
+
let(:profile_path) { "config/profiles/svg_1_1_full_static.yml" }
|
|
9
|
+
|
|
10
|
+
def requirement_for(rule)
|
|
11
|
+
SvgConform::Profile.load_from_file(profile_path).requirements.find do |req|
|
|
12
|
+
req.respond_to?(:rule) && req.rule == rule
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def run(rule, xml, mode: :sax, serialized: false)
|
|
17
|
+
requirement = requirement_for(rule)
|
|
18
|
+
profile = SvgConform::Profile.new(name: "t", requirements: [requirement])
|
|
19
|
+
if mode == :dom
|
|
20
|
+
profile.validate(SvgConform::Document.from_content(xml)).errors
|
|
21
|
+
else
|
|
22
|
+
SvgConform::SaxDocument.from_content(xml, source_serialized: serialized)
|
|
23
|
+
.validate_with_profile(profile).errors
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def doc(attrs: 'version="1.1"', body: '<rect width="1" height="1"/>', prolog: "")
|
|
28
|
+
%(<?xml version="1.0"?>\n#{prolog}<svg #{svg_ns} #{attrs}>#{body}</svg>)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "defines one entry per matrix row with its own id and citation" do
|
|
32
|
+
entries = SvgConform::Profile.load_from_file(profile_path).requirements
|
|
33
|
+
.grep(described_class)
|
|
34
|
+
expect(entries.map(&:rule)).to eq(described_class::RULES)
|
|
35
|
+
expect(entries.map(&:id)).to eq(
|
|
36
|
+
%w[svg11.document.xml svg11.document.root svg11.document.version
|
|
37
|
+
svg11.document.grammar svg11.document.css2
|
|
38
|
+
svg11.document.xml_stylesheet],
|
|
39
|
+
)
|
|
40
|
+
expect(entries.map(&:citation_uri)).to all(start_with("https://www.w3.org/TR/"))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "is registered in Requirements.all" do
|
|
44
|
+
expect(SvgConform::Requirements.all).to include(described_class)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "rejects an unknown rule" do
|
|
48
|
+
requirement = described_class.new(id: "x", rule: "nope")
|
|
49
|
+
context = SvgConform::ValidationContext.new(nil, nil, source: "<svg/>")
|
|
50
|
+
expect { requirement.validate_source("<svg/>", context) }
|
|
51
|
+
.to raise_error(ArgumentError, /unknown rule/)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
%i[sax dom].each do |mode|
|
|
55
|
+
context "when validating in #{mode} mode" do
|
|
56
|
+
it "accepts a valid document in every rule" do
|
|
57
|
+
described_class::RULES.each do |rule|
|
|
58
|
+
expect(run(rule, doc, mode: mode)).to be_empty, "rule #{rule}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "flags a wrong root namespace" do
|
|
63
|
+
errors = run("root", doc.sub(svg_ns, 'xmlns="http://example.com/x"'), mode: mode)
|
|
64
|
+
expect(errors.map(&:requirement_id)).to eq(["svg11.document.root"])
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "flags a wrong or missing version" do
|
|
68
|
+
expect(run("version", doc(attrs: 'version="1.0"'), mode: mode).size).to eq(1)
|
|
69
|
+
expect(run("version", doc(attrs: ""), mode: mode).size).to eq(1)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "flags DTD content-model and attribute violations" do
|
|
73
|
+
model = run("grammar", doc(body: "<rect><g/></rect>"), mode: mode)
|
|
74
|
+
attribute = run("grammar", doc(body: '<rect width="1" height="1" bogus="1"/>'), mode: mode)
|
|
75
|
+
expect(model.map(&:requirement_id)).to all(eq("svg11.document.grammar"))
|
|
76
|
+
expect(model).not_to be_empty
|
|
77
|
+
expect(attribute).not_to be_empty
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "does not follow an external entity" do
|
|
81
|
+
xml = "<?xml version=\"1.0\"?>\n<!DOCTYPE svg [<!ENTITY x SYSTEM \"file:///etc/passwd\">]>\n<svg #{svg_ns} version=\"1.1\"><title>&x;</title></svg>"
|
|
82
|
+
errors = run("grammar", xml, mode: mode)
|
|
83
|
+
expect(errors.map(&:message).join).to include("external")
|
|
84
|
+
expect(errors.map(&:message).join).not_to include("root:")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it "flags CSS grammar errors in style attributes and style elements" do
|
|
88
|
+
attr = run("css2", doc(body: '<rect width="1" height="1" style="fill: {red"/>'), mode: mode)
|
|
89
|
+
sheet = run("css2", doc(body: "<style>rect { fill: red </style>"), mode: mode)
|
|
90
|
+
expect(attr.size).to be >= 1
|
|
91
|
+
expect(sheet.size).to be >= 1
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "accepts valid CSS and skips non-CSS style types" do
|
|
95
|
+
css = "<style>rect { fill: red }</style><style type=\"text/x-other\">{{</style>"
|
|
96
|
+
expect(run("css2", doc(body: css), mode: mode)).to be_empty
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it "treats text/css media types case-insensitively" do
|
|
100
|
+
upper = run("css2", doc(body: '<style type="TEXT/CSS">rect { fill: red </style>'), mode: mode)
|
|
101
|
+
mixed = run("css2", doc(body: '<style type="Text/Css">rect { fill: red </style>'), mode: mode)
|
|
102
|
+
pi = %(<?xml-stylesheet href="a.css" type="TEXT/CSS"?>\n)
|
|
103
|
+
expect(upper.size).to be >= 1
|
|
104
|
+
expect(mixed.size).to be >= 1
|
|
105
|
+
expect(run("xml_stylesheet", doc(prolog: pi), mode: mode)).to be_empty
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "validates xml-stylesheet pseudo-attributes" do
|
|
109
|
+
good = doc(prolog: %(<?xml-stylesheet href="a.css" type="text/css"?>\n))
|
|
110
|
+
no_href = doc(prolog: %(<?xml-stylesheet type="text/css"?>\n))
|
|
111
|
+
bad_type = doc(prolog: %(<?xml-stylesheet href="a.xsl" type="text/xsl"?>\n))
|
|
112
|
+
garbled = doc(prolog: %(<?xml-stylesheet href=a.css?>\n))
|
|
113
|
+
expect(run("xml_stylesheet", good, mode: mode)).to be_empty
|
|
114
|
+
[no_href, bad_type, garbled].each do |xml|
|
|
115
|
+
expect(run("xml_stylesheet", xml, mode: mode).map(&:requirement_id))
|
|
116
|
+
.to eq(["svg11.document.xml_stylesheet"])
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it "reports malformed XML through SAX" do
|
|
123
|
+
errors = run("xml", "<svg #{svg_ns}><rect></svg>")
|
|
124
|
+
expect(errors.map(&:requirement_id)).to include("svg11.document.xml")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it "cannot prove declaration claims for a re-serialized source" do
|
|
128
|
+
errors = run("xml", doc, serialized: true)
|
|
129
|
+
expect(errors.map(&:requirement_id)).to eq(["svg11.document.xml"])
|
|
130
|
+
expect(errors.first.message).to include("cannot be proved")
|
|
131
|
+
expect(run("xml", doc, serialized: false)).to be_empty
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it "reports the same through a DOM node and a serialized SAX source" do
|
|
135
|
+
node = SvgConform::Document.from_content(doc).moxml_document
|
|
136
|
+
dom = SvgConform::Document.from_node(node)
|
|
137
|
+
profile = SvgConform::Profile.new(name: "t", requirements: [requirement_for("xml")])
|
|
138
|
+
expect(profile.validate(dom).errors.map(&:requirement_id))
|
|
139
|
+
.to eq(["svg11.document.xml"])
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "svg_conform/validation/css2_core_grammar"
|
|
5
|
+
|
|
6
|
+
RSpec.describe SvgConform::Validation::Css2CoreGrammar do
|
|
7
|
+
describe ".tokenize" do
|
|
8
|
+
def types(css)
|
|
9
|
+
described_class.tokenize(css).map(&:type)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "classifies the CSS 2 token kinds" do
|
|
13
|
+
css = 'a:hover #id 10 50% 2em url(x.png) rgb( "s" U+0-7F ~= |= @page <!-- -->'
|
|
14
|
+
expect(types(css).reject { |t| t == :s }).to eq(
|
|
15
|
+
[:ident, ":", :ident, :hash, :number, :percentage, :dimension, :uri, :function, :string,
|
|
16
|
+
:unicode_range, :includes, :dashmatch, :atkeyword, :cdo, :cdc],
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "drops comments and tracks line and column" do
|
|
21
|
+
tokens = described_class.tokenize("a /* c\n */ b")
|
|
22
|
+
expect(tokens.map(&:value)).to eq(["a", " ", " ", "b"])
|
|
23
|
+
expect(tokens.last.line).to eq(2)
|
|
24
|
+
expect(tokens.last.column).to eq(5)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "flags unterminated strings and comments" do
|
|
28
|
+
expect(types('"abc')).to eq([:bad_string])
|
|
29
|
+
expect(types("/* abc")).to eq([:bad_comment])
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "accepts escapes in identifiers" do
|
|
33
|
+
expect(types('\66 ill')).to eq([:ident])
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
describe ".parse_stylesheet" do
|
|
38
|
+
valid = {
|
|
39
|
+
"empty sheet" => "",
|
|
40
|
+
"comments and CDO/CDC" => "<!-- /* x */ a { fill: red } -->",
|
|
41
|
+
"multiple selectors" => "rect, circle.a > path[x=\"1\"]:first-child { fill: #fff }",
|
|
42
|
+
"empty declaration slots" => "a { ; fill: red;; stroke: blue; }",
|
|
43
|
+
"functions and !important" => "a { fill: rgb(1, 2, 3) !important; stroke: url(#g) }",
|
|
44
|
+
"empty ruleset" => "a { }",
|
|
45
|
+
"@import" => '@import url("x.css") screen;',
|
|
46
|
+
"@media with nested rules" => "@media print { a { x: y } b { x: y } }",
|
|
47
|
+
"@font-face" => '@font-face { font-family: "F"; src: url(f.woff) }',
|
|
48
|
+
"unknown at-rule block" => "@foo bar { baz { qux: 1 } }",
|
|
49
|
+
"selector-less ruleset" => "{ fill: red }",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
valid.each do |name, css|
|
|
53
|
+
it "accepts #{name}" do
|
|
54
|
+
expect(described_class.parse_stylesheet(css)).to be_valid
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
invalid = {
|
|
59
|
+
"missing colon" => ["a { fill red }", /expected ':'/],
|
|
60
|
+
"missing value" => ["a { fill: }", /no value/],
|
|
61
|
+
"missing property" => ["a { : red }", /property name/],
|
|
62
|
+
"unclosed ruleset" => ["a { fill: red", /missing '}'/],
|
|
63
|
+
"unclosed string" => ["a { fill: \"red }", /unterminated string/],
|
|
64
|
+
"unclosed comment" => ["a { fill: red } /* x", /unterminated comment/],
|
|
65
|
+
"unclosed function" => ["a { fill: rgb(1,2 }", /missing '\)'/],
|
|
66
|
+
"selector without block" => ["a b", /missing a '\{' block/],
|
|
67
|
+
"unterminated at-rule" => ["@import url(x.css)", /at-rule/],
|
|
68
|
+
"stray closing brace" => ["a { fill: red } }", /unexpected '\}'/],
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
invalid.each do |name, (css, message)|
|
|
72
|
+
it "rejects #{name}" do
|
|
73
|
+
result = described_class.parse_stylesheet(css)
|
|
74
|
+
expect(result).not_to be_valid
|
|
75
|
+
expect(result.errors.map(&:message).join("\n")).to match(message)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "reports the position of the first error" do
|
|
80
|
+
error = described_class.parse_stylesheet("a { fill: red }\nb { fill blue }")
|
|
81
|
+
.errors.first
|
|
82
|
+
expect(error.line).to eq(2)
|
|
83
|
+
expect(error.to_s).to include("line 2")
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
describe ".parse_declarations" do
|
|
88
|
+
it "accepts a style attribute value" do
|
|
89
|
+
css = "fill:red; stroke : url(#a) ; stroke-width:2px;"
|
|
90
|
+
expect(described_class.parse_declarations(css)).to be_valid
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it "accepts an empty value and stray semicolons" do
|
|
94
|
+
expect(described_class.parse_declarations("")).to be_valid
|
|
95
|
+
expect(described_class.parse_declarations(" ; ;")).to be_valid
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it "rejects a declaration without a colon" do
|
|
99
|
+
result = described_class.parse_declarations("fill red")
|
|
100
|
+
expect(result).not_to be_valid
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "rejects a stray closing brace in a declaration list" do
|
|
104
|
+
result = described_class.parse_declarations("fill:red } stroke:blue")
|
|
105
|
+
expect(result).not_to be_valid
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "rejects a ruleset where a declaration list is expected" do
|
|
109
|
+
expect(described_class.parse_declarations("a { fill: red }")).not_to be_valid
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "does not judge property names or values" do
|
|
113
|
+
expect(described_class.parse_declarations("bogus-prop: nonsense 12qq")).to be_valid
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "digest"
|
|
5
|
+
require "yaml"
|
|
6
|
+
require "tmpdir"
|
|
7
|
+
require "svg_conform/validation/svg_1_1_document_conformance"
|
|
8
|
+
|
|
9
|
+
# Path is fixed by the card; the cop expects svg11_ instead of svg_1_1_.
|
|
10
|
+
RSpec.describe SvgConform::Validation::Svg11DocumentConformance do # rubocop:disable RSpec/SpecFilePathFormat
|
|
11
|
+
subject(:conformance) { described_class.new }
|
|
12
|
+
|
|
13
|
+
let(:ns) { %(xmlns="http://www.w3.org/2000/svg" version="1.1") }
|
|
14
|
+
let(:dtd_path) { described_class::DTD_PATH }
|
|
15
|
+
|
|
16
|
+
def svg(body, attrs = ns)
|
|
17
|
+
%(<?xml version="1.0"?>\n<svg #{attrs}>#{body}</svg>)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Library selection record (card 03, "Open implementation question").
|
|
21
|
+
# Probes: valid document, invalid content model, external-entity attempt,
|
|
22
|
+
# Appendix G.2 foreign content, run offline against the vendored flat DTD.
|
|
23
|
+
#
|
|
24
|
+
# Moxml/Leptris 1.9.381 (Leptris::XML::DTD): parses the DTD but ignores
|
|
25
|
+
# conditional sections whose keyword is a parameter entity
|
|
26
|
+
# (<![%svg-structure.module;[ ... ]]>), so no SVG element is declared and
|
|
27
|
+
# every probe, including the invalid ones, reports valid. Rejected.
|
|
28
|
+
# Nokogiri 1.19 (libxml2 2.13, Ruby >= 3.1 with 1.16/1.18): all four
|
|
29
|
+
# probes behave correctly offline, in-process, with NONET and no entity
|
|
30
|
+
# loading. Selected.
|
|
31
|
+
describe "library probes" do
|
|
32
|
+
let(:flat_dtd) { File.read(dtd_path) }
|
|
33
|
+
|
|
34
|
+
it "records that Leptris cannot enforce the flat DTD" do
|
|
35
|
+
require "leptris"
|
|
36
|
+
handle = Leptris::XML::DTD.parse(flat_dtd)
|
|
37
|
+
invalid = Leptris::XML.parse(%(<svg #{ns}><rect width="1" height="1"><g/></rect></svg>))
|
|
38
|
+
|
|
39
|
+
expect(handle.validate(invalid)).to be_empty
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "probe 1: Nokogiri accepts a valid document" do
|
|
43
|
+
expect(conformance.validate(svg(%(<rect width="1" height="1"/>))).issues).to be_empty
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "probe 2: Nokogiri rejects an invalid content model" do
|
|
47
|
+
result = conformance.validate(svg(%(<rect width="1" height="1"><g/></rect>)))
|
|
48
|
+
|
|
49
|
+
expect(result.issues.map(&:kind)).to eq([:dtd])
|
|
50
|
+
expect(result.issues.first.message).to include("rect content does not follow the DTD")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "probe 3: an external entity attempt is rejected without reading the file" do
|
|
54
|
+
Dir.mktmpdir do |dir|
|
|
55
|
+
secret = File.join(dir, "secret.txt")
|
|
56
|
+
File.write(secret, "SECRET-MARKER")
|
|
57
|
+
source = %(<?xml version="1.0"?><!DOCTYPE svg [<!ENTITY x SYSTEM "file://#{secret}">]>) +
|
|
58
|
+
%(<svg #{ns}><title>&x;</title></svg>)
|
|
59
|
+
|
|
60
|
+
result = conformance.validate(source)
|
|
61
|
+
|
|
62
|
+
expect(result.issues.map(&:kind)).to eq([:external_entity])
|
|
63
|
+
expect(result.transformed).to be_nil
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "probe 4: foreign content validates after the Appendix G.2 transform" do
|
|
68
|
+
source = svg(%(<f:thing f:a="1"><g/></f:thing><metadata><f:m/></metadata>),
|
|
69
|
+
%(#{ns} xmlns:f="urn:foreign"))
|
|
70
|
+
|
|
71
|
+
expect(conformance.validate(source).issues).to be_empty
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
describe "#validate" do
|
|
76
|
+
it "reports a missing required attribute" do
|
|
77
|
+
result = conformance.validate(svg(%(<rect height="1"/>)))
|
|
78
|
+
|
|
79
|
+
expect(result.issues.first.message).to include("Element rect does not carry attribute width")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "reports an undeclared attribute" do
|
|
83
|
+
result = conformance.validate(svg(%(<rect width="1" height="1" bogus="x"/>)))
|
|
84
|
+
|
|
85
|
+
expect(result.issues.first.message).to include("No declaration for attribute bogus")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "reports malformed XML" do
|
|
89
|
+
result = conformance.validate(svg(%(<rect width="1" height="1">)))
|
|
90
|
+
|
|
91
|
+
expect(result.issues.first.kind).to eq(:malformed_xml)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "reports a document element outside the SVG namespace" do
|
|
95
|
+
result = conformance.validate(%(<svg xmlns="urn:other" version="1.1"/>))
|
|
96
|
+
|
|
97
|
+
expect(result.issues.map(&:kind)).to eq([:no_svg_root])
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it "validates xlink attributes under any prefix" do
|
|
101
|
+
source = svg(%(<defs><g id="a"/></defs><use xlink:href="#a"/>),
|
|
102
|
+
%(#{ns} xmlns:xl="http://www.w3.org/1999/xlink"))
|
|
103
|
+
source = source.sub("xlink:href", "xl:href")
|
|
104
|
+
|
|
105
|
+
expect(conformance.validate(source).issues).to be_empty
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "validates prefixed SVG elements" do
|
|
109
|
+
source = %(<s:svg xmlns:s="http://www.w3.org/2000/svg" version="1.1"><s:rect width="1" height="1"/></s:svg>)
|
|
110
|
+
|
|
111
|
+
expect(conformance.validate(source).issues).to be_empty
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it "expands internal entities before validating" do
|
|
115
|
+
source = %(<?xml version="1.0"?><!DOCTYPE svg [<!ENTITY w "5">]><svg #{ns}><rect width="&w;" height="1"/></svg>)
|
|
116
|
+
|
|
117
|
+
expect(conformance.validate(source).issues).to be_empty
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it "never modifies the caller's source" do
|
|
121
|
+
source = svg(%(<f:x xmlns:f="urn:f"/><rect width="1" height="1"/>)).freeze
|
|
122
|
+
|
|
123
|
+
expect { conformance.validate(source) }.not_to raise_error
|
|
124
|
+
expect(source).to include("<f:x")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it "keeps the Appendix G.2 copy free of foreign content" do
|
|
128
|
+
copy = conformance.validate(svg(%(<f:x xmlns:f="urn:f"/>), %(#{ns} f2:a="1" xmlns:f2="urn:f"))).transformed
|
|
129
|
+
|
|
130
|
+
expect(copy).not_to include("f:x")
|
|
131
|
+
expect(copy).not_to include("f2:a")
|
|
132
|
+
expect(copy).to include(%(xmlns:xlink="http://www.w3.org/1999/xlink"))
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
describe "vendored DTD" do
|
|
137
|
+
it "matches the manifest checksum" do
|
|
138
|
+
entry = YAML.safe_load_file(described_class::MANIFEST_PATH, permitted_classes: [Date])["files"].first
|
|
139
|
+
|
|
140
|
+
expect(Digest::SHA256.file(dtd_path).hexdigest).to eq(entry["sha256"])
|
|
141
|
+
expect(entry["source_url"]).to start_with("https://www.w3.org/")
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
|
|
6
|
+
RSpec.describe SvgConform::ValidationContext, "#source" do
|
|
7
|
+
let(:source) do
|
|
8
|
+
<<~XML
|
|
9
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
10
|
+
<!-- exact bytes -->
|
|
11
|
+
<?xml-stylesheet href="a.css" type="text/css"?>
|
|
12
|
+
<svg xmlns="http://www.w3.org/2000/svg" version="1.1"><rect width="1" height="1"/></svg>
|
|
13
|
+
XML
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
let(:recorder_class) do
|
|
17
|
+
Class.new(SvgConform::Requirements::BaseRequirement) do
|
|
18
|
+
attr_accessor :calls
|
|
19
|
+
|
|
20
|
+
def validate_source(source, context)
|
|
21
|
+
(@calls ||= []) << {
|
|
22
|
+
source: source, path: context.source_path,
|
|
23
|
+
serialized: context.source_serialized?, order: :source
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def validate_sax_element(_element, _context)
|
|
28
|
+
(@calls ||= []) << { order: :element }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def check(_node, _context)
|
|
32
|
+
(@calls ||= []) << { order: :element }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
let(:requirement) { recorder_class.new(id: "rec") }
|
|
38
|
+
let(:profile) { SvgConform::Profile.new(name: "rec", requirements: [requirement]) }
|
|
39
|
+
|
|
40
|
+
def sources_seen
|
|
41
|
+
requirement.calls.select { |c| c[:order] == :source }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "keeps the BaseRequirement hook a no-op" do
|
|
45
|
+
base = SvgConform::Requirements::BaseRequirement.new(id: "b")
|
|
46
|
+
expect(base.validate_source("x", nil)).to be_nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "stores source and path on ValidationContext" do
|
|
50
|
+
ctx = described_class.new(nil, nil, source: "s",
|
|
51
|
+
source_path: "p.svg")
|
|
52
|
+
expect([ctx.source, ctx.source_path, ctx.source_serialized?])
|
|
53
|
+
.to eq(["s", "p.svg", false])
|
|
54
|
+
expect(described_class.new(nil, nil).source).to be_nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it "retains exact content in Document for string and file inputs" do
|
|
58
|
+
Dir.mktmpdir do |dir|
|
|
59
|
+
path = File.join(dir, "a.svg")
|
|
60
|
+
File.write(path, source)
|
|
61
|
+
expect(SvgConform::Document.from_content(source).source).to eq(source)
|
|
62
|
+
from_file = SvgConform::Document.from_file(path)
|
|
63
|
+
expect(from_file.source).to eq(source)
|
|
64
|
+
expect(from_file.source_serialized?).to be(false)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "marks Document.from_node source as serialized" do
|
|
69
|
+
node = SvgConform::Document.from_content(source).moxml_document
|
|
70
|
+
doc = SvgConform::Document.from_node(node)
|
|
71
|
+
expect(doc.source_serialized?).to be(true)
|
|
72
|
+
expect(doc.source).to include("<svg")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it "passes SaxDocument content into the handler" do
|
|
76
|
+
SvgConform::SaxDocument.from_content(source).validate_with_profile(profile)
|
|
77
|
+
expect(sources_seen.size).to eq(1)
|
|
78
|
+
expect(sources_seen.first).to include(source: source, path: nil,
|
|
79
|
+
serialized: false)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "calls validate_source exactly once, before elements, in DOM and SAX" do
|
|
83
|
+
profile.validate(SvgConform::Document.from_content(source))
|
|
84
|
+
dom_orders = requirement.calls.map { |c| c[:order] }
|
|
85
|
+
expect(dom_orders.first).to eq(:source)
|
|
86
|
+
expect(dom_orders.count(:source)).to eq(1)
|
|
87
|
+
|
|
88
|
+
requirement.calls = []
|
|
89
|
+
SvgConform::SaxDocument.from_content(source).validate_with_profile(profile)
|
|
90
|
+
sax_orders = requirement.calls.map { |c| c[:order] }
|
|
91
|
+
expect(sax_orders.first).to eq(:source)
|
|
92
|
+
expect(sax_orders.count(:source)).to eq(1)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "gives file entry points identical source and path in both modes" do
|
|
96
|
+
Dir.mktmpdir do |dir|
|
|
97
|
+
path = File.join(dir, "a.svg")
|
|
98
|
+
File.write(path, source)
|
|
99
|
+
|
|
100
|
+
profile.validate_file(path)
|
|
101
|
+
sax = sources_seen.first
|
|
102
|
+
requirement.calls = []
|
|
103
|
+
profile.validate(SvgConform::Document.from_file(path))
|
|
104
|
+
dom = sources_seen.first
|
|
105
|
+
|
|
106
|
+
expect(sax).to include(source: source, path: path, serialized: false)
|
|
107
|
+
expect(dom).to eq(sax)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it "flags serialized source for DOM built from a node" do
|
|
112
|
+
node = SvgConform::Document.from_content(source).moxml_document
|
|
113
|
+
profile.validate(SvgConform::Document.from_node(node))
|
|
114
|
+
expect(sources_seen.first[:serialized]).to be(true)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it "still runs source validation when SAX parsing fails" do
|
|
118
|
+
SvgConform::SaxDocument.from_content("<svg><unclosed>")
|
|
119
|
+
.validate_with_profile(profile)
|
|
120
|
+
expect(sources_seen.size).to eq(1)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -258,6 +258,45 @@ RSpec.describe SvgConform::ValidationContext do
|
|
|
258
258
|
|
|
259
259
|
expect(context.infos.first.message).to eq("test notice")
|
|
260
260
|
end
|
|
261
|
+
|
|
262
|
+
it "stores an explicit requirement ID on the info issue" do
|
|
263
|
+
context.add_notice(rule: nil, node: Object.new, message: "n",
|
|
264
|
+
requirement_id: "my_requirement")
|
|
265
|
+
|
|
266
|
+
expect(context.infos.first.requirement_id).to eq("my_requirement")
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
it "keeps existing calls working without a requirement ID" do
|
|
270
|
+
context.add_notice(rule: Struct.new(:id).new("test_rule"),
|
|
271
|
+
node: Object.new, message: "n")
|
|
272
|
+
|
|
273
|
+
expect(context.infos.first.requirement_id).to eq("test_rule")
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
describe "#add_external_reference_notice" do
|
|
278
|
+
let(:reference) do
|
|
279
|
+
SvgConform::References::ReferenceClassifier.classify(
|
|
280
|
+
"https://example.com/x", element_name: "a", attribute_name: "href"
|
|
281
|
+
)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
it "stores the explicit requirement ID and JSON-safe data" do
|
|
285
|
+
context.add_external_reference_notice(node: Object.new,
|
|
286
|
+
reference: reference,
|
|
287
|
+
requirement_id: "link_validation")
|
|
288
|
+
|
|
289
|
+
info = context.infos.first
|
|
290
|
+
expect(info.requirement_id).to eq("link_validation")
|
|
291
|
+
expect(info.data[:reference][:value]).to eq("https://example.com/x")
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
it "keeps existing calls working" do
|
|
295
|
+
context.add_external_reference_notice(node: Object.new,
|
|
296
|
+
reference: reference)
|
|
297
|
+
|
|
298
|
+
expect(context.infos.size).to eq(1)
|
|
299
|
+
end
|
|
261
300
|
end
|
|
262
301
|
|
|
263
302
|
describe "#has_errors?" do
|