Skip to content

Commit 966ad03

Browse files
authored
Merge pull request #6497 from joewiz/bugfix/xmldb-store-binary-xml-mime-npe
[bugfix] xmldb:store: parse binary content stored under an XML mime type
2 parents 231e220 + f89a80a commit 966ad03

3 files changed

Lines changed: 143 additions & 12 deletions

File tree

exist-core/src/main/java/org/exist/xquery/functions/xmldb/XMLDBStore.java

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,15 @@
3232
import java.nio.file.StandardCopyOption;
3333
import java.util.Properties;
3434

35+
import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream;
3536
import org.apache.logging.log4j.LogManager;
3637
import org.apache.logging.log4j.Logger;
3738

3839
import org.exist.storage.serializers.EXistOutputKeys;
3940
import org.exist.util.FileUtils;
4041
import org.exist.util.MimeTable;
4142
import org.exist.util.MimeType;
43+
import org.exist.util.StringInputSource;
4244
import org.exist.util.io.TemporaryFileManager;
4345
import org.exist.util.serializer.SAXSerializer;
4446
import org.exist.xmldb.EXistResource;
@@ -185,18 +187,40 @@ public Sequence evalWithCollection(Collection collection, Sequence[] args, Seque
185187
if (Type.subTypeOf(item.getType(), Type.STRING)) {
186188
resource.setContent(item.getStringValue());
187189
} else if (item.getType() == Type.BASE64_BINARY) {
188-
// Pass the BinaryValue through rather than .toJavaObject(), which reads the
189-
// whole value into a heap byte[] (a multi-GB upload would OOM). The local
190-
// resource keeps the BinaryValue and the store streams it via the
191-
// (disk-backed by default) binary cache instead of materializing it.
192190
final BinaryValue binaryValue = (BinaryValue) item;
193-
// The resource is only lent the value: closing the resource (on exit from
194-
// this try-with-resources) closes the value it was given, but the query
195-
// that produced the value may still need to read it afterwards. Take a
196-
// shared reference on the resource's behalf so its close() releases only
197-
// that reference.
198-
binaryValue.incrementSharedReferences();
199-
resource.setContent(binaryValue);
191+
if (mimeType.isXMLType()) {
192+
// The content is binary but the target mime type is an XML type: parse the
193+
// binary's bytes as an XML document. Setting the BinaryValue directly would
194+
// leave the (XML) resource with no character/byte stream, and the store would
195+
// later fail with an NPE when it tried to parse a null string as XML. Buffer
196+
// the bytes into a re-readable StringInputSource: the xmldb:store route parses
197+
// the source twice (validate, then store) and does not reset it between passes,
198+
// so a single-shot stream would be drained on the second pass. Buffering costs
199+
// nothing asymptotically here — storing XML parses the whole document into a
200+
// DOM anyway — and StringInputSource yields a fresh (unsynchronized) stream on
201+
// each read, letting the parser detect the encoding from the bytes.
202+
final byte[] xmlBytes;
203+
try (final UnsynchronizedByteArrayOutputStream baos = new UnsynchronizedByteArrayOutputStream()) {
204+
binaryValue.streamBinaryTo(baos);
205+
xmlBytes = baos.toByteArray();
206+
} catch (final IOException e) {
207+
throw new XPathException(this, "Unable to read binary content to store as XML: " + e.getMessage(), e);
208+
}
209+
resource.setContent(new StringInputSource(xmlBytes));
210+
} else {
211+
// Pass the BinaryValue through rather than .toJavaObject(), which reads the
212+
// whole value into a heap byte[] (a multi-GB upload would OOM). The local
213+
// resource keeps the BinaryValue and the store streams it via the
214+
// (disk-backed by default) binary cache instead of materializing it.
215+
//
216+
// The resource is only lent the value: closing the resource (on exit from
217+
// this try-with-resources) closes the value it was given, but the query
218+
// that produced the value may still need to read it afterwards. Take a
219+
// shared reference on the resource's behalf so its close() releases only
220+
// that reference.
221+
binaryValue.incrementSharedReferences();
222+
resource.setContent(binaryValue);
223+
}
200224
} else if (Type.subTypeOf(item.getType(), Type.NODE)) {
201225
if (mimeType.isXMLType()) {
202226
final ContentHandler handler = ((XMLResource) resource).setContentAsSAX();

exist-core/src/main/java/org/exist/xquery/value/BinaryValueFromInputStream.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ public InputStream getInputStream() {
115115
} catch (InstantiationException ex) {
116116
LOG.error(ex.getMessage(), ex);
117117
}
118-
return null;
118+
return InputStream.nullInputStream();
119119
}
120120

121121
@Override
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
(:
2+
: eXist-db Open Source Native XML Database
3+
: Copyright (C) 2001 The eXist-db Authors
4+
:
5+
: info@exist-db.org
6+
: http://www.exist-db.org
7+
:
8+
: This library is free software; you can redistribute it and/or
9+
: modify it under the terms of the GNU Lesser General Public
10+
: License as published by the Free Software Foundation; either
11+
: version 2.1 of the License, or (at your option) any later version.
12+
:
13+
: This library is distributed in the hope that it will be useful,
14+
: but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16+
: Lesser General Public License for more details.
17+
:
18+
: You should have received a copy of the GNU Lesser General Public
19+
: License along with this library; if not, write to the Free Software
20+
: Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21+
:)
22+
xquery version "3.1";
23+
24+
(:~
25+
: Tests for xmldb:store() with a binary (xs:base64Binary) content value.
26+
:
27+
: Regression for the case where a binary content value is stored under an XML-type
28+
: mime (explicit mime="application/xml" or inferred from an .xml resource name):
29+
: the store used to fail with java.lang.NullPointerException
30+
: ("Cannot invoke \"String.length()\" because \"<parameter1>\" is null") because the
31+
: binary was bound to an XML resource that had no character/byte stream to parse.
32+
: The bytes should instead be parsed as an XML document.
33+
:)
34+
module namespace sbt = "http://exist-db.org/xquery/test/xmldb-store-binary";
35+
36+
declare namespace test = "http://exist-db.org/xquery/xqsuite";
37+
declare namespace util = "http://exist-db.org/xquery/util";
38+
39+
declare variable $sbt:collection-name := "xmldb-store-binary-test";
40+
declare variable $sbt:collection := "/db/" || $sbt:collection-name;
41+
42+
declare
43+
%test:setUp
44+
function sbt:setup() as empty-sequence() {
45+
let $_ := xmldb:create-collection("/db", $sbt:collection-name)
46+
return ()
47+
};
48+
49+
declare
50+
%test:tearDown
51+
function sbt:tear-down() as empty-sequence() {
52+
let $_ := xmldb:remove($sbt:collection)
53+
return ()
54+
};
55+
56+
(: binary content + explicit application/xml mime -> parsed and stored as XML :)
57+
declare
58+
%test:assertEquals("1")
59+
function sbt:binary-with-xml-mime-is-parsed() {
60+
let $bin := util:string-to-binary("<doc><a>1</a></doc>")
61+
let $stored := xmldb:store($sbt:collection, "explicit.xml", $bin, "application/xml")
62+
return doc($stored)/doc/a/string()
63+
};
64+
65+
(: binary content, mime inferred from the .xml resource name -> parsed and stored as XML :)
66+
declare
67+
%test:assertEquals("1")
68+
function sbt:binary-with-inferred-xml-mime-is-parsed() {
69+
let $bin := util:string-to-binary("<doc><a>1</a></doc>")
70+
let $stored := xmldb:store($sbt:collection, "inferred.xml", $bin)
71+
return doc($stored)/doc/a/string()
72+
};
73+
74+
(: the parsed document is a real XML document, not a binary resource :)
75+
declare
76+
%test:assertEquals("false")
77+
function sbt:binary-with-xml-mime-is-not-binary() {
78+
let $bin := util:string-to-binary("<doc><a>1</a></doc>")
79+
let $stored := xmldb:store($sbt:collection, "as-xml.xml", $bin, "application/xml")
80+
return string(util:binary-doc-available($stored))
81+
};
82+
83+
(: XML encoding declaration in the bytes is honored (parser reads it from the stream) :)
84+
declare
85+
%test:assertEquals("ä")
86+
function sbt:binary-with-xml-mime-honors-encoding() {
87+
let $bin := util:string-to-binary("<?xml version='1.0' encoding='UTF-8'?><doc>&#xE4;</doc>")
88+
let $stored := xmldb:store($sbt:collection, "encoded.xml", $bin, "application/xml")
89+
return doc($stored)/doc/string()
90+
};
91+
92+
(: control: binary content + a binary mime is still stored byte-for-byte as a binary resource :)
93+
declare
94+
%test:assertEquals("true")
95+
function sbt:binary-with-binary-mime-stays-binary() {
96+
let $bin := util:string-to-binary("<doc><a>1</a></doc>")
97+
let $stored := xmldb:store($sbt:collection, "raw.bin", $bin, "application/octet-stream")
98+
return string(util:binary-doc-available($stored))
99+
};
100+
101+
(: malformed XML bytes under an XML mime fail with a clean store/parse error, NOT an NPE :)
102+
declare
103+
%test:assertError("storing document")
104+
function sbt:binary-malformed-xml-mime-reports-parse-error() {
105+
let $bin := util:string-to-binary("not well-formed <")
106+
return xmldb:store($sbt:collection, "broken.xml", $bin, "application/xml")
107+
};

0 commit comments

Comments
 (0)