marcel 1.2.1 → 2.0.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4631da96baa71a625b60ec1f66ed05c4f6966f62340a0e6c522f67133dd39e48
4
- data.tar.gz: 61c1b75b32db1aa7ab69716fb42f0eb7b12d05090d5e56738a29254311fa1a21
3
+ metadata.gz: fdf325e94b9a73831f75c2b618a433bdaba1fc59437b18024420e6d72abd4027
4
+ data.tar.gz: 180c5324b58129e5035f72f45b0ea6aa8f2bf9c42594f36fef9a194304685a00
5
5
  SHA512:
6
- metadata.gz: f8e1a5b1fce489a8ea20dccbf88b2771bb9441f94631377155e30b8bb5be04738beb13688e0b007c75112283323cd2b577e398825b070195d0bf3f4ef770b140
7
- data.tar.gz: cfa87feac79dfc6c1f462a85533295c51f837a7ba9423f461309e8b6c41690a2dc163bba8da60608ccea3a860e14c9d554e40a0edd94a5116cb9565b0d23d4e3
6
+ metadata.gz: 14c1c71fe176a0b51b719cb35346add6f68ad72e57b4832636645fd66d311670ea4852bb1f9af4e92e688cb9f39ad562212e2a91541dd383b16d1093dc1bb218
7
+ data.tar.gz: 3b2bacd966149657d9958220c3a93a8bb26273cabc8422a6f8739364425e659b4b6c927283961f65cf900162fe545da284a15c11cc329b3e085d6ed892bd0e03
data/README.md CHANGED
@@ -5,11 +5,29 @@ Marcel chooses the most appropriate content type for a file by inspecting its co
5
5
  Marcel checks, in order:
6
6
 
7
7
  1. The "magic bytes" sniffed from the file contents.
8
- 2. The declared type, typically provided in a Content-Type header on an uploaded file, unless it's the `application/octet-stream` default.
8
+ 2. The declared type, typically provided in a Content-Type header on an uploaded file, if it is a valid single media type other than the `application/octet-stream` default.
9
9
  3. The filename extension.
10
- 4. Safe fallback to the indeterminate `application/octet-stream` default.
10
+ 4. Conservative fallback to the indeterminate `application/octet-stream` default.
11
11
 
12
- At each step, the most specific MIME subtype is selected. This allows the declared type and file extension to refine the parent type sniffed from the file contents, but not conflict with it. For example, if "file.csv" has declared type `text/plain`, `text/csv` is returned since it's a more specific subtype of `text/plain`. Similarly, Adobe Illustrator files are PDFs internally, so magic byte sniffing indicates `application/pdf` which is refined to `application/illustrator` by the `ai` file extension. But a PDF named "image.png" will still be detected as `application/pdf` since `image/png` is not a subtype.
12
+ At each step, the most specific MIME subtype is selected. This allows the declared type and file extension to refine the parent type sniffed from the file contents, but not conflict with it. For example, if "file.csv" has declared type `text/plain`, `text/csv` is returned since it's a more specific subtype of `text/plain`. Similarly, Adobe Illustrator files are PDFs internally, so magic byte sniffing indicates `application/pdf` which is refined to `application/illustrator` by the `ai` file extension. But a PDF named "image.png" will still be detected as `application/pdf` since `image/png` is not a subtype. Specificity is based on MIME taxonomy, not proof that a file conforms to the selected type; see Security considerations below.
13
+
14
+ Declared types may include parameters and surrounding HTTP whitespace. As a deliberate compatibility recovery, Marcel tolerates a single trailing semicolon. It ignores other malformed and comma-separated declared types rather than choosing one value from a list.
15
+
16
+ ## Security considerations
17
+
18
+ Marcel is a best-effort file type labeler, not a file validator or a security boundary. The declared type and filename are caller-provided hints. A syntactically valid but unregistered declared type may be returned as-is when content magic does not conflict with it.
19
+
20
+ When content magic identifies a ZIP-based container, Marcel reads the archive's central directory listing — a bounded read from the end of seekable content — to distinguish Office document families and their macro-enabled variants by catalogued part names such as `xl/vbaProject.bin`. It reads only member names, not member contents, and does not verify format conformance or archive contents. A non-macro label is not proof that macros are absent: unseekable or truncated content, malformed archives, and formats catalogued under other names skip this refinement, and hints may still refine the generic container type. Likewise, `Marcel::Magic#text?` and `#image?` describe MIME taxonomy, not whether content is safe.
21
+
22
+ When content magic identifies generic XML, Marcel scans a bounded 64 KiB prefix for the document's root element and refines the label by the root's namespace and local name, following Apache Tika's root-element rules: feeds, KML, property lists, XSLT, XHTML and Office 2003 XML among others. The scan resolves no entities, DTD declarations, or external resources; as with Tika's namespace-aware SAX extraction, a prolog or root start-tag that is not well-formed at the token level — including namespace rules, forbidden characters, and bytes invalid in the declared encoding — refines nothing. Like Tika, it labels un-namespaced `<body>`, `<p>`, `<script>`, `<frameset>`, `<iframe>` and `<link>` roots as `text/html`. Prologs it cannot read, roots beyond the prefix, and unknown roots keep the generic `application/xml` label, which is therefore not proof that the content is inert markup.
23
+
24
+ Do not use Marcel's result by itself to decide whether content is safe to execute, parse with privileged features, or render inline. Apply the controls required by the consuming parser or renderer independently of the detected label.
25
+
26
+ Magic rules inspect bounded samples, so results can differ when a caller provides only a prefix. HTML detection recognizes document-opening `<html>` and `<!DOCTYPE html>` markers, not every fragment that can contain active markup. A result other than `text/html` is therefore not proof that the bytes are safe markup.
27
+
28
+ Serve untrusted uploads as attachments, restrict inline rendering to a small vetted allowlist, or isolate them on a separate untrusted origin. Also set an explicit `Content-Type` from a trusted source and `X-Content-Type-Options: nosniff`; those headers do not make correctly labelled active content safe to render inline. Rails Active Storage applies separate content-type and disposition controls when serving blobs. Call `Marcel::Magic.by_magic` directly when caller-provided filename and declared-type hints need to be evaluated separately from content magic.
29
+
30
+ Callers should apply their normal request and metadata size limits before invoking Marcel. Declared MIME types larger than 8 KiB are ignored. Content IOs must support `rewind`; buffer pipes and sockets before detection.
13
31
 
14
32
  ## Usage
15
33
 
@@ -35,7 +53,7 @@ Marcel::MimeType.for extension: ".pdf"
35
53
  Marcel::MimeType.for Pathname.new("unrecognisable-data"), name: "example", declared_type: "image/png"
36
54
  # => "image/png"
37
55
 
38
- # Safe fallback to application/octet-stream
56
+ # Conservative fallback to application/octet-stream
39
57
  Marcel::MimeType.for StringIO.new(File.read "unrecognisable-data")
40
58
  # => "application/octet-stream"
41
59
  ```
@@ -50,14 +68,16 @@ Marcel::MimeType.for name: "file.customtxt"
50
68
  # => "text/custom"
51
69
  ```
52
70
 
71
+ Registration mutates a process-global registry. Add custom types during single-threaded application boot, before concurrent detection begins. Extension collisions replace the existing mapping, and removing the custom type does not restore a mapping it replaced.
72
+
53
73
  ## Motivation
54
74
 
55
- Marcel was extracted from Basecamp's file detection heuristics. The aim is provide sensible, safe, "do what I expect" results for typical file handling. Test fixtures have been added for many common file types, including those typically encountered by Basecamp.
75
+ Marcel was extracted from Basecamp's file detection heuristics. The aim is to provide sensible, conservative, "do what I expect" results for typical file handling. Test fixtures have been added for many common file types, including those typically encountered by Basecamp.
56
76
 
57
77
 
58
78
  ## Contributing
59
79
 
60
- Marcel generates MIME lookup tables with `bundle exec rake update`. MIME types are seeded from data found in `data/*.xml`. Custom MIMEs may be added to `data/custom.xml`, while overrides to the standard MIME database may be added to `lib/marcel/mime_type/definitions.rb`.
80
+ Marcel generates MIME lookup tables with `bundle exec rake update`. MIME types are seeded from data found in `data/*.xml`. The Apache Tika download and committed `data/tika.xml` are verified against the commit and checksum in `script/download_tika_data.rb`; review and update both values when refreshing it. Custom MIMEs may be added to `data/custom.xml`, while code-defined overrides live in the definitions files under `lib/marcel`.
61
81
 
62
82
  Marcel follows the same contributing guidelines as [rails/rails](https://github.com/rails/rails#contributing).
63
83
 
data/SECURITY.md ADDED
@@ -0,0 +1,7 @@
1
+ # Security Policy
2
+
3
+ Please do not report suspected security vulnerabilities in a public GitHub issue. Report them privately through the [Ruby on Rails security process](https://rubyonrails.org/security).
4
+
5
+ Marcel is a best-effort MIME type labeler, not a file validator or security boundary. It labels content; it does not sanitize it, and a result other than `text/html` is not proof that the bytes are safe markup. Serve untrusted uploads as attachments, through a strict inline allowlist, or from a separate untrusted origin. Set an explicit `Content-Type` from a trusted source and `X-Content-Type-Options: nosniff` as additional controls. See the Security considerations section of the README for details.
6
+
7
+ Include the Marcel version, the inputs supplied to `Marcel::MimeType.for` (bytes, filename, declared type, and whether the bytes are a partial read), the returned type, and the downstream security decision in the report.
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ html_magic = [
4
+ [0..4096, %r{\A(?:\xEF\xBB\xBF)?[ \t\r\n\f]*(?:<\?xml(?:[ \t\r\n\f]+(?:[^?]|\?(?!>))*)?\?>[ \t\r\n\f]*)?(?:<!--(?:[^-]|-(?!->))*-->[ \t\r\n\f]*)*(?:<!DOCTYPE[ \t\r\n\f]+html(?=[ \t\r\n\f>])|<html(?=[ \t\r\n\f>]))}imn],
5
+ ]
6
+
7
+ xhtml_magic = [
8
+ [0..4096, %r{
9
+ \A
10
+ (?:\xEF\xBB\xBF)?[ \t\r\n\f]*
11
+ (?:<\?xml(?:[ \t\r\n\f]+(?:[^?]|\?(?!>))*)?\?>[ \t\r\n\f]*)?
12
+ (?:<!--(?:[^-]|-(?!->))*-->[ \t\r\n\f]*)*
13
+ (?:
14
+ <!DOCTYPE[ \t\r\n\f]+html(?:[ \t\r\n\f]+[^>]*)?>[ \t\r\n\f]*
15
+ (?:<!--(?:[^-]|-(?!->))*-->[ \t\r\n\f]*)*
16
+ )?
17
+ <html(?=[ \t\r\n\f>])
18
+ (?:
19
+ [ \t\r\n\f]+
20
+ (?!xmlns[ \t\r\n\f]*=)
21
+ [A-Za-z_:][A-Za-z0-9_.:-]*
22
+ [ \t\r\n\f]*=[ \t\r\n\f]*
23
+ (?:"[^"]*"|'[^']*')
24
+ )*
25
+ [ \t\r\n\f]+xmlns[ \t\r\n\f]*=[ \t\r\n\f]*
26
+ (?:"http://www\.w3\.org/1999/xhtml"|'http://www\.w3\.org/1999/xhtml')
27
+ }xmn],
28
+ ]
29
+
30
+ [
31
+ ["text/html", html_magic],
32
+ ["application/xhtml+xml", xhtml_magic],
33
+ ].each do |type, magic|
34
+ extensions = Marcel::TYPE_EXTS[type]
35
+ parents = Marcel::TYPE_PARENTS[type]
36
+
37
+ Marcel::Magic.remove(type)
38
+ Marcel::Magic.add(type, extensions: extensions, parents: parents, magic: magic)
39
+ end
@@ -0,0 +1,488 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Marcel
4
+ class Magic
5
+ # Procedural refinement of XML documents by their root element.
6
+ #
7
+ # Many XML vocabularies — feeds, KML, property lists, XSLT, Office 2003 XML — share the
8
+ # same leading bytes, and a prolog of arbitrary length (declaration, comments, DOCTYPE)
9
+ # can precede the element that tells them apart. Apache Tika identifies them by the root
10
+ # element's namespace and local name instead, and those rules are generated into
11
+ # ROOT_XML. This module scans a bounded prefix past the prolog to the root start-tag,
12
+ # resolves the root's namespace from its own xmlns declarations, and looks the pair up
13
+ # to refine a generic application/xml match.
14
+ #
15
+ # Tika hands the prefix to a namespace-aware SAX parser and takes no root when parsing
16
+ # fails before the first start element, so the scan holds the same line at the token
17
+ # level. Input is brought to UTF-8 before scanning — UTF-16 strictly, legacy encodings
18
+ # with invalid sequences replaced and the document refused if a replacement lands
19
+ # before the end of the root start-tag — so the bytes consumed to that point must be
20
+ # valid in the document's encoding and free of characters the XML version forbids. The
21
+ # XML declaration may appear only first, parsed in full and limited to versions 1.0 and
22
+ # 1.1; no other processing instruction may use the reserved xml target, and a PI's data
23
+ # must be separated from its target by whitespace; comments may not contain "--". The
24
+ # root and attribute QNames and namespace prefixes are validated as Unicode NCNames,
25
+ # while PI targets and reference names admit the full XML Name, whose colon namespace
26
+ # processing leaves alone. Attribute values may not contain "<"; their
27
+ # character references must denote characters the XML version admits, and their entity
28
+ # references must be predefined unless a DTD that could declare them was seen.
29
+ # References are validated, never resolved; nothing beyond the tokens is — no entities,
30
+ # no DTD declarations, no external resources — and anything the scanner cannot read
31
+ # with certainty leaves the generic type in place.
32
+ module Xml
33
+ REFINABLE_TYPE = "application/xml"
34
+
35
+ # Tika examines at most this much of a document for its root element.
36
+ MAX_SCAN = 64 * 1024
37
+
38
+ UTF8_BOM = "\xEF\xBB\xBF".b
39
+ UTF16LE_BOM = "\xFF\xFE".b
40
+ UTF16BE_BOM = "\xFE\xFF".b
41
+
42
+ # Byte-level scanner pattern for names: XML Name restricted to ASCII plus any
43
+ # non-ASCII byte. It only locates tokens; every captured name is then validated
44
+ # against the real Unicode name grammar (NCNAME and XML_NAME below) after decoding.
45
+ NAME = '[A-Za-z_:\x80-\xFF][A-Za-z0-9._:\-\x80-\xFF]*'
46
+ SPACE = '[ \t\r\n]'
47
+ ENCODING_NAME = '[A-Za-z][A-Za-z0-9._\-]*'
48
+
49
+ WHITESPACE = /\G#{SPACE}*/n
50
+ START_TAG = /\G<(#{NAME})/n
51
+ ATTRIBUTE = /\G#{SPACE}+(#{NAME})#{SPACE}*=#{SPACE}*(?:"([^"<]*)"|'([^'<]*)')/n
52
+ START_TAG_END = /\G#{SPACE}*\/?>/n
53
+ PROCESSING_INSTRUCTION = /\G<\?(#{NAME})/n
54
+ RESERVED_TARGET = /\A[Xx][Mm][Ll]\z/n
55
+
56
+ # The complete declaration grammar: a SAX-supported version, then optionally encoding,
57
+ # then optionally standalone, in that order, with nothing else before ?>. Matched in
58
+ # full so that trailing or duplicated tokens cannot hide behind the PI skipper.
59
+ XML_DECLARATION = /
60
+ \A<\?xml
61
+ #{SPACE}+version#{SPACE}*=#{SPACE}*(?:"(?<version>1\.[01])"|'(?<version>1\.[01])')
62
+ (?:#{SPACE}+encoding#{SPACE}*=#{SPACE}*(?:"(?<encoding>#{ENCODING_NAME})"|'(?<encoding>#{ENCODING_NAME})'))?
63
+ (?:#{SPACE}+standalone#{SPACE}*=#{SPACE}*(?:"(?:yes|no)"|'(?:yes|no)'))?
64
+ #{SPACE}*\?>
65
+ /xn
66
+
67
+ # Entity and character references; anything else after & in an attribute value is a
68
+ # well-formedness error. The CharRef grammar puts no cap on digits — leading zeros
69
+ # are legal in any number — so digit runs are unbounded here and their magnitude is
70
+ # bounded lexically by valid_character_reference?.
71
+ REFERENCE = /&(?:(?<name>#{NAME})|\#(?<decimal>[0-9]+)|\#x(?<hex>[0-9A-Fa-f]+));/n
72
+
73
+ # The five entities every XML document predefines; any other entity reference is only
74
+ # potentially declared when the document carries a DTD.
75
+ PREDEFINED_ENTITIES = %w( amp lt gt apos quot ).freeze
76
+
77
+ # NameStartChar and NameChar under the XML 1.0 (5th ed.) Unicode name grammar,
78
+ # colon excluded, matched against decoded UTF-8 names.
79
+ NAME_START_CHARS = "A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u{10000}-\u{EFFFF}"
80
+ NAME_CHARS = "#{NAME_START_CHARS}\\-.0-9\u00B7\u0300-\u036F\u203F-\u2040"
81
+
82
+ # QName parts and namespace prefixes must be NCNames — namespace processing claims
83
+ # their colon — where PI targets and entity names keep the full Name grammar, in
84
+ # which the colon is an ordinary name character.
85
+ NCNAME = /\A[#{NAME_START_CHARS}][#{NAME_CHARS}]*\z/
86
+ XML_NAME = /\A[:#{NAME_START_CHARS}][:#{NAME_CHARS}]*\z/
87
+
88
+ # Characters each XML version forbids as literals, matched against the decoded
89
+ # UTF-8 text. XML 1.1 restricts the C0 and C1 controls (NEL excepted) to character
90
+ # references, where XML 1.0 forbids C0 outright but admits C1 literals.
91
+ FORBIDDEN_CHARS = {
92
+ "1.0" => /[\x00-\x08\x0B\x0C\x0E-\x1F\uFFFE\uFFFF]/,
93
+ "1.1" => /[\x00-\x08\x0B\x0C\x0E-\x1F\u007F-\u0084\u0086-\u009F\uFFFE\uFFFF]/,
94
+ }.freeze
95
+
96
+ XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace"
97
+ XMLNS_NAMESPACE = "http://www.w3.org/2000/xmlns/"
98
+
99
+ PROCESSING_INSTRUCTION_OPEN = "<?".b
100
+ PROCESSING_INSTRUCTION_CLOSE = "?>".b
101
+ COMMENT_OPEN = "<!--".b
102
+ COMMENT_CLOSE = "-->".b
103
+ DOUBLE_HYPHEN = "--".b
104
+ DOCTYPE_OPEN = "<!DOCTYPE".b
105
+ DOCTYPE_DELIMITER = /["'\[\]<>]/n
106
+ SPACE_BYTES = [0x20, 0x09, 0x0D, 0x0A].freeze
107
+
108
+ # Ruby resolves these to a process default rather than to a charset.
109
+ PSEUDO_ENCODING_NAMES = %w( locale external filesystem ).freeze
110
+
111
+ class << self
112
+ # Returns the type ROOT_XML assigns to the IO's root element if +base_type+ is the
113
+ # generic XML type and the root element can be read, or +base_type+ unchanged.
114
+ # Partial reads, roots past MAX_SCAN, malformed or mis-encoded prologs, namespace
115
+ # errors and unknown roots refine nothing: the base type stands.
116
+ def refine(io, base_type)
117
+ return base_type unless base_type == REFINABLE_TYPE
118
+
119
+ io = StringIO.new(io.to_s) unless io.respond_to?(:read)
120
+
121
+ root = begin
122
+ if decoded = decode(read_prefix(io))
123
+ root_element(*decoded)
124
+ end
125
+ rescue StandardError
126
+ nil
127
+ ensure
128
+ begin
129
+ io.rewind
130
+ rescue StandardError
131
+ nil
132
+ end
133
+ end
134
+
135
+ type = root && ROOT_XML[root]
136
+ type ? Magic.canonical(type) : base_type
137
+ end
138
+
139
+ private
140
+
141
+ def read_prefix(io)
142
+ io.rewind
143
+ prefix = "".b
144
+ while prefix.bytesize < MAX_SCAN && (chunk = io.read(MAX_SCAN - prefix.bytesize))
145
+ break if chunk.empty?
146
+ prefix << chunk.b
147
+ end
148
+ prefix.byteslice(0, MAX_SCAN)
149
+ end
150
+
151
+ # Returns [bytes to scan, text mode for valid_text?, XML version], or nil when
152
+ # the prefix cannot be decoded. Scanning always reads UTF-8 bytes: the magic gate
153
+ # guarantees a byte order mark on UTF-16 input, which is transcoded strictly up
154
+ # front; documents already in UTF-8 are scanned raw; and any other declared
155
+ # encoding is replace-transcoded, since its multibyte characters may carry ASCII
156
+ # trail bytes (Shift_JIS ソ ends in an ASCII backslash) that would derail a raw
157
+ # byte scan.
158
+ def decode(prefix)
159
+ if prefix.start_with?(UTF16LE_BOM)
160
+ data = transcode(prefix.byteslice(2..), Encoding::UTF_16LE)
161
+ if declaration = xml_declaration(data)
162
+ [data, :utf8, declaration[0]]
163
+ end
164
+ elsif prefix.start_with?(UTF16BE_BOM)
165
+ data = transcode(prefix.byteslice(2..), Encoding::UTF_16BE)
166
+ if declaration = xml_declaration(data)
167
+ [data, :utf8, declaration[0]]
168
+ end
169
+ else
170
+ data = prefix.start_with?(UTF8_BOM) ? prefix.byteslice(3..) : prefix
171
+ if declaration = xml_declaration(data)
172
+ version, name = declaration
173
+ encoding = name ? find_encoding(name) : Encoding::UTF_8
174
+ if encoding == Encoding::UTF_8
175
+ [data, :utf8, version]
176
+ elsif encoding
177
+ [reencode(data, encoding), :replaced, version]
178
+ end
179
+ end
180
+ end
181
+ end
182
+
183
+ # A code unit or surrogate pair split by the scan limit is the one sequence that
184
+ # may legitimately be incomplete: it is dropped, since it lies beyond the scan.
185
+ # Any other invalid sequence raises and refines nothing.
186
+ def transcode(bytes, encoding)
187
+ bytes = bytes.byteslice(0, bytes.bytesize & ~1).force_encoding(encoding)
188
+ begin
189
+ bytes.encode(Encoding::UTF_8).b
190
+ rescue EncodingError
191
+ bytes.byteslice(0, bytes.bytesize - 2).encode(Encoding::UTF_8).b
192
+ end
193
+ end
194
+
195
+ # Replace-transcodes a legacy-encoded prefix to UTF-8. Invalid and unconvertible
196
+ # sequences become U+FFFD, which legacy charsets cannot themselves encode, so a
197
+ # replacement within the consumed prefix always marks input a SAX parser would
198
+ # reject — valid_text? refuses it — while replacements past the root, like bad
199
+ # bytes trailing the document or a character split by MAX_SCAN, are never
200
+ # examined. (The rare charset that can encode U+FFFD, like GB18030, only
201
+ # forfeits a refinement.)
202
+ def reencode(bytes, encoding)
203
+ bytes.dup.force_encoding(encoding) \
204
+ .encode(Encoding::UTF_8, invalid: :replace, undef: :replace).b
205
+ end
206
+
207
+ # Returns [version, declared encoding name or nil] when the document either has no
208
+ # XML declaration (version defaults to 1.0) or opens with one that is well-formed
209
+ # in full, and nil when it opens with a malformed declaration or a processing
210
+ # instruction that misuses the xml target.
211
+ def xml_declaration(data)
212
+ return ["1.0", nil] unless data.start_with?(PROCESSING_INSTRUCTION_OPEN)
213
+ return nil unless target = PROCESSING_INSTRUCTION.match(data, 0)
214
+ return ["1.0", nil] unless target[1].match?(RESERVED_TARGET)
215
+
216
+ if declaration = XML_DECLARATION.match(data)
217
+ [declaration[:version], declaration[:encoding]]
218
+ end
219
+ end
220
+
221
+ def find_encoding(name)
222
+ return nil if PSEUDO_ENCODING_NAMES.include?(name.downcase)
223
+
224
+ encoding = Encoding.find(name)
225
+ encoding if encoding.ascii_compatible? && !encoding.dummy? && encoding != Encoding::BINARY
226
+ rescue ArgumentError
227
+ nil
228
+ end
229
+
230
+ # Skips the prolog — whitespace, processing instructions (including the XML
231
+ # declaration), comments and the document type declaration — and returns the
232
+ # root element's [namespace, local name], or nil if no root start-tag is found.
233
+ def root_element(data, mode, version)
234
+ pos = 0
235
+ doctype_seen = false
236
+ while pos
237
+ pos = WHITESPACE.match(data, pos).end(0)
238
+
239
+ if data.byteslice(pos, 2) == PROCESSING_INSTRUCTION_OPEN
240
+ pos = skip_processing_instruction(data, pos)
241
+ elsif data.byteslice(pos, 4) == COMMENT_OPEN
242
+ pos = skip_comment(data, pos)
243
+ elsif data.byteslice(pos, 9) == DOCTYPE_OPEN
244
+ doctype_seen = true
245
+ pos = skip_doctype(data, pos + 9)
246
+ else
247
+ return parse_start_tag(data, pos, mode, version, doctype_seen)
248
+ end
249
+ end
250
+ end
251
+
252
+ # The XML declaration is the one processing instruction allowed the xml target,
253
+ # and only as the very first bytes of the document (checked by xml_declaration).
254
+ # A PI's data, when present, must be separated from its target by whitespace.
255
+ def skip_processing_instruction(data, pos)
256
+ return nil unless target = PROCESSING_INSTRUCTION.match(data, pos)
257
+ return nil if pos != 0 && target[1].match?(RESERVED_TARGET)
258
+ return nil unless pos == 0 && target[1].match?(RESERVED_TARGET) || valid_name?(target[1], XML_NAME)
259
+
260
+ after = target.end(0)
261
+ return nil unless data.byteslice(after, 2) == PROCESSING_INSTRUCTION_CLOSE ||
262
+ SPACE_BYTES.include?(data.getbyte(after))
263
+
264
+ skip_past(data, PROCESSING_INSTRUCTION_CLOSE, after)
265
+ end
266
+
267
+ # A comment ends at its first "--", which must be the one closing it.
268
+ def skip_comment(data, pos)
269
+ close = data.index(DOUBLE_HYPHEN, pos + 4)
270
+ close + 3 if close && data.byteslice(close, 3) == COMMENT_CLOSE
271
+ end
272
+
273
+ def skip_past(data, terminator, pos)
274
+ close = data.index(terminator, pos)
275
+ close + terminator.bytesize if close
276
+ end
277
+
278
+ # Advances past a document type declaration, honouring quoted literals and the
279
+ # bracketed internal subset so that markup declarations like
280
+ # <!ENTITY x "<foo>"> and comments within the subset cannot end the scan early.
281
+ def skip_doctype(data, pos)
282
+ depth = 0
283
+ while pos && (pos = data.index(DOCTYPE_DELIMITER, pos))
284
+ case data.getbyte(pos)
285
+ when 0x22, 0x27 # " '
286
+ pos = skip_past(data, data.byteslice(pos, 1), pos + 1)
287
+ when 0x5B # [
288
+ depth += 1
289
+ pos += 1
290
+ when 0x5D # ]
291
+ depth -= 1
292
+ pos += 1
293
+ when 0x3C # <
294
+ if data.byteslice(pos, 4) == COMMENT_OPEN
295
+ pos = skip_comment(data, pos)
296
+ elsif data.byteslice(pos, 2) == PROCESSING_INSTRUCTION_OPEN
297
+ pos = skip_processing_instruction(data, pos)
298
+ else
299
+ pos += 1
300
+ end
301
+ else # >
302
+ return pos + 1 if depth <= 0
303
+ pos += 1
304
+ end
305
+ end
306
+ end
307
+
308
+ # Reads the root start-tag's qualified name and attributes. Attribute values are
309
+ # parsed as quoted literals so a > within one cannot truncate the tag; the tag must
310
+ # close within the scanned prefix, and everything consumed up to that point must be
311
+ # valid in the document's encoding and free of forbidden characters.
312
+ def parse_start_tag(data, pos, mode, version, doctype_seen)
313
+ return nil unless tag = START_TAG.match(data, pos)
314
+
315
+ qualified_name = tag[1]
316
+ pos = tag.end(0)
317
+
318
+ attributes = {}
319
+ while attribute = ATTRIBUTE.match(data, pos)
320
+ # Duplicate attributes are a well-formedness error (Unique Att Spec).
321
+ return nil if attributes.key?(attribute[1])
322
+
323
+ attributes[attribute[1]] = attribute[2] || attribute[3]
324
+ pos = attribute.end(0)
325
+ end
326
+ return nil unless tag_end = START_TAG_END.match(data, pos)
327
+ return nil unless valid_text?(data.byteslice(0, tag_end.end(0)), mode, version)
328
+
329
+ bindings = namespace_bindings(attributes, version)
330
+ return nil unless bindings
331
+ return nil unless valid_attributes?(attributes, bindings, version, doctype_seen)
332
+
333
+ resolve_name(qualified_name, bindings)
334
+ end
335
+
336
+ # The consumed prefix must be free of the characters its XML version forbids as
337
+ # literals, wherever they fall — comment, PI, DOCTYPE or attribute — since a SAX
338
+ # parser rejects them before the root. Raw and strictly transcoded input must be
339
+ # valid UTF-8, in which a genuine U+FFFD literal is legal XML; replace-transcoded
340
+ # input must carry no U+FFFD, each one marking a sequence invalid in the declared
341
+ # encoding.
342
+ def valid_text?(consumed, mode, version)
343
+ consumed = consumed.force_encoding(Encoding::UTF_8)
344
+ sound = mode == :replaced ? !consumed.include?("\u{FFFD}") : consumed.valid_encoding?
345
+ sound && !consumed.match?(FORBIDDEN_CHARS.fetch(version))
346
+ end
347
+
348
+ # Names must satisfy the Unicode name grammar once decoded — NCNAME unless the
349
+ # caller passes the laxer XML_NAME — with the byte-level scanner accepting a
350
+ # superset purely to locate them. Every name is UTF-8 by the time it gets here:
351
+ # decode leaves only raw UTF-8 documents untranscoded.
352
+ def valid_name?(name, grammar = NCNAME)
353
+ name = name.dup.force_encoding(Encoding::UTF_8)
354
+ name.valid_encoding? && grammar.match?(name)
355
+ end
356
+
357
+ # A character reference must denote a character the XML version admits: never a
358
+ # surrogate, a noncharacter, or a code point beyond U+10FFFF; XML 1.0 admits only
359
+ # tab, newline, carriage return and #x20 up, while XML 1.1 admits everything from
360
+ # #x1 — including as references the control characters it forbids as literals.
361
+ def valid_character_scalar?(value, version)
362
+ return false if value > 0x10FFFF || value.between?(0xD800, 0xDFFF) ||
363
+ value == 0xFFFE || value == 0xFFFF
364
+
365
+ if version == "1.1"
366
+ value >= 0x1
367
+ else
368
+ value == 0x9 || value == 0xA || value == 0xD || value >= 0x20
369
+ end
370
+ end
371
+
372
+ # A character reference admits unlimited leading zeros, so the scalar's magnitude
373
+ # is bounded lexically — at most seven significant decimal digits or six hex
374
+ # cover every code point through U+10FFFF — before conversion, never by
375
+ # converting an unbounded digit run. All zeros denote #x0, rejected as a scalar.
376
+ def valid_character_reference?(digits, base, version)
377
+ digits = digits.sub(/\A0+/, "")
378
+ digits.length <= (base == 16 ? 6 : 7) && valid_character_scalar?(digits.to_i(base), version)
379
+ end
380
+
381
+ # Every & in an attribute value must begin a well-formed reference: a character
382
+ # reference to an admissible character, a predefined entity, or — only when a DTD
383
+ # that could declare it was seen — any well-named entity. Never resolved.
384
+ def valid_references?(value, version, doctype_seen)
385
+ well_formed = true
386
+ rest = value.gsub(REFERENCE) do
387
+ reference = Regexp.last_match
388
+ well_formed &&= if digits = reference[:decimal]
389
+ valid_character_reference?(digits, 10, version)
390
+ elsif digits = reference[:hex]
391
+ valid_character_reference?(digits, 16, version)
392
+ else
393
+ valid_name?(reference[:name], XML_NAME) &&
394
+ (doctype_seen || PREDEFINED_ENTITIES.include?(reference[:name]))
395
+ end
396
+ ""
397
+ end
398
+ well_formed && !rest.include?("&")
399
+ end
400
+
401
+ # Splits a QName into [prefix or nil, local name]; more than one colon or an empty
402
+ # part is a namespace error, returned as nil.
403
+ def split_qualified_name(name)
404
+ if name.include?(":")
405
+ prefix, local_name = name.split(":", 2)
406
+ [prefix, local_name] unless prefix.empty? || local_name.empty? || local_name.include?(":")
407
+ else
408
+ [nil, name]
409
+ end
410
+ end
411
+
412
+ # The tag's namespace declarations as {prefix or nil => namespace or nil}, with
413
+ # the xml prefix implicitly bound. The xml and xmlns names and namespaces are
414
+ # reserved. A prefix bound to an empty namespace is an undeclaration, permitted by
415
+ # Namespaces 1.1 (except for xml) and an error under 1.0; violations are namespace
416
+ # errors, returned as nil.
417
+ def namespace_bindings(attributes, version)
418
+ bindings = { nil => nil, "xml" => XML_NAMESPACE }
419
+ attributes.each do |name, value|
420
+ if name == "xmlns"
421
+ return nil if value == XML_NAMESPACE || value == XMLNS_NAMESPACE
422
+ bindings[nil] = value.empty? ? nil : value
423
+ elsif name.start_with?("xmlns:")
424
+ prefix = name.byteslice(6..)
425
+ return nil if prefix.empty? || prefix.include?(":") || prefix == "xmlns"
426
+
427
+ if value.empty?
428
+ return nil unless version == "1.1" && prefix != "xml"
429
+
430
+ bindings.delete(prefix)
431
+ else
432
+ return nil if (prefix == "xml") != (value == XML_NAMESPACE)
433
+ return nil if value == XMLNS_NAMESPACE
434
+
435
+ bindings[prefix] = value
436
+ end
437
+ end
438
+ end
439
+ bindings
440
+ end
441
+
442
+ # Ordinary attributes resolved by expanded name, as a namespace-aware parser sees
443
+ # them: names must be valid NCNames, prefixes bound, [namespace, local name] pairs
444
+ # unique (the default namespace does not apply to attributes), and values free of
445
+ # malformed references.
446
+ def valid_attributes?(attributes, bindings, version, doctype_seen)
447
+ expanded = {}
448
+ attributes.each do |name, value|
449
+ return false unless valid_references?(value, version, doctype_seen)
450
+
451
+ if name == "xmlns"
452
+ next
453
+ elsif name.start_with?("xmlns:")
454
+ return false unless valid_name?(name.byteslice(6..))
455
+ next
456
+ end
457
+
458
+ return false unless split = split_qualified_name(name)
459
+
460
+ prefix, local_name = split
461
+ return false unless valid_name?(local_name)
462
+ return false if prefix && !(valid_name?(prefix) && bindings.key?(prefix))
463
+
464
+ key = [prefix && bindings[prefix], local_name]
465
+ return false if expanded.key?(key)
466
+
467
+ expanded[key] = true
468
+ end
469
+ true
470
+ end
471
+
472
+ # Resolves the root element's name against the tag's own bindings: the default
473
+ # namespace for an unprefixed name, the prefix's binding otherwise. An unbound
474
+ # prefix or invalid name is a namespace error.
475
+ def resolve_name(qualified_name, bindings)
476
+ return nil unless split = split_qualified_name(qualified_name)
477
+
478
+ prefix, local_name = split
479
+ return nil unless valid_name?(local_name)
480
+ return nil if prefix == "xmlns"
481
+ return nil if prefix && !(valid_name?(prefix) && bindings.key?(prefix))
482
+
483
+ [prefix ? bindings[prefix] : bindings[nil], local_name]
484
+ end
485
+ end
486
+ end
487
+ end
488
+ end