Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2a7838b
[bugfix] Security-gated file:// URI resolution for fn:doc, fn:unparse…
joewiz Mar 16, 2026
47c7d02
[bugfix] Exclude 'xmlns' prefix from fn:in-scope-prefixes result
joewiz Mar 26, 2026
b958ff3
[bugfix] Strip BOM from fn:unparsed-text and fn:unparsed-text-lines r…
joewiz Mar 26, 2026
2244ee5
[bugfix] Fix gDay/gMonth/gYear timezone comparison and xs:token white…
joewiz Mar 27, 2026
4e3d43d
[bugfix] Add missing error codes to XPathExceptions across core
joewiz Mar 27, 2026
3450edf
[bugfix] Fix error codes for abstract type casts and string validation
joewiz Mar 16, 2026
2b70a9d
[bugfix] Fix format-number negative exponent zero-padding
joewiz Mar 17, 2026
eee1a0f
[bugfix] Fix JSON serialization: XDM mode bypass and duplicate key de…
joewiz Mar 24, 2026
b1efb60
[bugfix] Check dynamically available text resources in fn:json-doc
joewiz Mar 26, 2026
b63c1a1
[bugfix] Allow empty sequence as options arg in fn:parse-json
joewiz Mar 31, 2026
2a58e2f
[bugfix] Fix deep-equal, fn:round overflow, and Unicode codepoint com…
joewiz Apr 4, 2026
7cfa528
[bugfix] Fix XPTY0004 vs FORG0001 error codes for impossible casts
joewiz Mar 22, 2026
28310ce
[bugfix] Allow mixed numeric type comparisons in fn:min and fn:max
joewiz Mar 22, 2026
67e0f39
[bugfix] Throw FOCA0002 for NaN/INF cast to integer/decimal types
joewiz Mar 22, 2026
14a0a9f
[bugfix] Support casting xs:boolean to integer subtypes
joewiz Mar 22, 2026
985d993
[bugfix] Fix fn:parse-ietf-date case sensitivity and edge cases
joewiz Mar 23, 2026
ba794b5
[bugfix] Fix fn:not() error on empty context sequence in predicates (…
joewiz Mar 5, 2026
bbc042b
[bugfix] Fix fn:not(.) failure on atomic sequences (#2308)
joewiz Mar 5, 2026
10dfeb1
[test] Add tests for simple map operator in predicates (#3289)
joewiz Mar 5, 2026
8e3a496
[bugfix] Normalize annotation values in XQSuite assertEquals for XML …
joewiz Mar 3, 2026
4e543e4
[bugfix] Fix // followed by reverse axis step being misinterpreted
joewiz Mar 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g
Original file line number Diff line number Diff line change
Expand Up @@ -2360,7 +2360,13 @@ throws PermissionDeniedException, EXistException, XPathException
(s.getTest().getType() == Type.ATTRIBUTE && s.getAxis() == Constants.CHILD_AXIS))
// combines descendant-or-self::node()/attribute:*
s.setAxis(Constants.DESCENDANT_ATTRIBUTE_AXIS);
else {
else if (s.getAxis() <= Constants.PRECEDING_SIBLING_AXIS) {
// Reverse axis: insert explicit descendant-or-self::node() step
LocationStep descStep = new LocationStep(context, Constants.DESCENDANT_SELF_AXIS, new TypeTest(Type.NODE));
descStep.setAbbreviated(true);
path.replaceLastExpression(descStep);
path.add(step);
} else {
s.setAxis(Constants.DESCENDANT_SELF_AXIS);
s.setAbbreviated(true);
}
Expand Down Expand Up @@ -2984,6 +2990,13 @@ throws PermissionDeniedException, EXistException, XPathException
rs.setAxis(Constants.DESCENDANT_AXIS);
} else if (rs.getAxis() == Constants.SELF_AXIS) {
rs.setAxis(Constants.DESCENDANT_SELF_AXIS);
} else if (rs.getAxis() <= Constants.PRECEDING_SIBLING_AXIS) {
// Reverse axis: cannot merge with descendant-or-self,
// insert explicit descendant-or-self::node() step before the reverse axis step
LocationStep descStep = new LocationStep(context, Constants.DESCENDANT_SELF_AXIS, new TypeTest(Type.NODE));
descStep.setAbbreviated(true);
path.replaceLastExpression(descStep);
path.add(rightStep);
} else {
rs.setAxis(Constants.DESCENDANT_SELF_AXIS);
rs.setAbbreviated(true);
Expand Down
19 changes: 18 additions & 1 deletion exist-core/src/main/java/org/exist/util/Collations.java
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,24 @@
*/
public static int compare(@Nullable final Collator collator, final String s1,final String s2) {
if (collator == null) {
return s1 == null ? (s2 == null ? 0 : -1) : s1.compareTo(s2);
if (s1 == null) {
return s2 == null ? 0 : -1;
}
// Compare by Unicode codepoints, not UTF-16 code units.
// String.compareTo() compares char (UTF-16) values, which gives wrong
// ordering for supplementary characters (U+10000+) encoded as surrogate pairs.
int i1 = 0, i2 = 0;

Check notice on line 355 in exist-core/src/main/java/org/exist/util/Collations.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

exist-core/src/main/java/org/exist/util/Collations.java#L355

Use one line for each declaration, it enhances code readability.
while (i1 < s1.length() && i2 < s2.length()) {
final int cp1 = s1.codePointAt(i1);
final int cp2 = s2.codePointAt(i2);
if (cp1 != cp2) {
return cp1 - cp2;
}
i1 += Character.charCount(cp1);
i2 += Character.charCount(cp2);
}
// Shorter string is less; equal length means equal
return (s1.length() - i1) - (s2.length() - i2);
} else {
return collator.compare(s1, s2);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,25 @@ private void serializeXML(final Sequence sequence, final int start, final int ho
}

private void serializeJSON(final Sequence sequence, final long compilationTime, final long executionTime) throws SAXException, XPathException {
// backwards compatibility: if the sequence contains a single element, we assume
// it should be transformed to JSON following the rules of the old JSON writer
if (sequence.hasOne() && (Type.subTypeOf(sequence.getItemType(), Type.DOCUMENT) || Type.subTypeOf(sequence.getItemType(), Type.ELEMENT))) {
// XDM serialization: use JSONSerializer for maps and arrays (W3C JSON output method).
// For element/document nodes, use the legacy XML-to-JSON conversion path for
// backward compatibility with eXist's traditional JSON serialization.
// TODO (eXist 8.0): Remove legacy XML-to-JSON conversion.
// The legacy path is deprecated in 7.0 — use fn:serialize($map, map{"method":"json"}) instead.
final boolean isXdmMapOrArray = sequence.hasOne()
&& (sequence.getItemType() == Type.MAP_ITEM || sequence.getItemType() == Type.ARRAY_ITEM);

if (isXdmMapOrArray || (!sequence.hasOne())
|| Type.subTypeOfUnion(sequence.getItemType(), Type.ANY_ATOMIC_TYPE)) {
// Maps, arrays, sequences, and atomic values: use W3C JSONSerializer
final JSONSerializer serializer = new JSONSerializer(broker, outputProperties);
serializer.serialize(sequence, writer);
} else if (sequence.hasOne()
&& (Type.subTypeOf(sequence.getItemType(), Type.DOCUMENT) || Type.subTypeOf(sequence.getItemType(), Type.ELEMENT))) {
// Legacy path: single element/document → XML-to-JSON conversion
serializeXML(sequence, 1, 1, false, false, compilationTime, executionTime);
} else {
JSONSerializer serializer = new JSONSerializer(broker, outputProperties);
final JSONSerializer serializer = new JSONSerializer(broker, outputProperties);
serializer.serialize(sequence, writer);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ public void serialize(Sequence sequence, Writer writer) throws SAXException {
if ("yes".equals(outputProperties.getProperty(OutputKeys.INDENT, "no"))) {
generator.useDefaultPrettyPrinter();
}
if ("yes".equals(outputProperties.getProperty(EXistOutputKeys.ALLOW_DUPLICATE_NAMES, "yes"))) {
// allow-duplicate-names=no (default per W3C) → enable strict detection
// allow-duplicate-names=yes → disable strict detection (allow duplicates)
if ("no".equals(outputProperties.getProperty(EXistOutputKeys.ALLOW_DUPLICATE_NAMES, "no"))) {
generator.enable(JsonGenerator.Feature.STRICT_DUPLICATE_DETECTION);
} else {
generator.disable(JsonGenerator.Feature.STRICT_DUPLICATE_DETECTION);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public void addValue(String value) {

public void addEnclosedExpr(Expression expr) throws XPathException {
if(isNamespaceDecl)
{throw new XPathException(this, "enclosed expressions are not allowed in namespace " +
{throw new XPathException(this, ErrorCodes.XQST0022, "enclosed expressions are not allowed in namespace " +
"declaration attributes");}
contents.add(expr);
}
Expand Down
14 changes: 11 additions & 3 deletions exist-core/src/main/java/org/exist/xquery/CastExpression.java
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,13 @@ public Sequence eval(final Sequence contextSequence, final Item contextItem) thr
}
}

// Should be handled by the parser
if (requiredType == Type.ANY_ATOMIC_TYPE || (requiredType == Type.NOTATION && expression.returnsType() != Type.NOTATION)) {
// XPST0080: cannot cast to abstract or special types
if (requiredType == Type.ANY_ATOMIC_TYPE || requiredType == Type.ANY_SIMPLE_TYPE
|| (requiredType == Type.NOTATION && expression.returnsType() != Type.NOTATION)) {
throw new XPathException(this, ErrorCodes.XPST0080, "cannot cast to " + Type.getTypeName(requiredType));
}

if (requiredType == Type.ANY_SIMPLE_TYPE || expression.returnsType() == Type.ANY_SIMPLE_TYPE || requiredType == Type.UNTYPED || expression.returnsType() == Type.UNTYPED) {
if (expression.returnsType() == Type.ANY_SIMPLE_TYPE || requiredType == Type.UNTYPED || expression.returnsType() == Type.UNTYPED) {
throw new XPathException(this, ErrorCodes.XPST0051, "cannot cast to " + Type.getTypeName(requiredType));
}

Expand Down Expand Up @@ -117,6 +118,13 @@ public Sequence eval(final Sequence contextSequence, final Item contextItem) thr
throw new XPathException(this, ErrorCodes.XPTY0004, "Cannot cast " + Type.getTypeName(item.getType()) + " to xs:QName");
}
} else {
// Per XPath F&O 3.1, Section 19: if the source and target types
// have no valid casting relationship, raise XPTY0004 (not FORG0001).
if (!Type.isCastable(item.getType(), requiredType)) {
throw new XPathException(this, ErrorCodes.XPTY0004,
"Cannot cast " + Type.getTypeName(item.getType()) +
" to " + Type.getTypeName(requiredType));
}
result = item.convertTo(requiredType);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,11 @@ public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathExc
{context.getProfiler().message(this, Profiler.START_SEQUENCES, "CONTEXT ITEM", contextItem.toSequence());}
}

if (requiredType == Type.ANY_ATOMIC_TYPE || (requiredType == Type.NOTATION && expression.returnsType() != Type.NOTATION))
if (requiredType == Type.ANY_ATOMIC_TYPE || requiredType == Type.ANY_SIMPLE_TYPE
|| (requiredType == Type.NOTATION && expression.returnsType() != Type.NOTATION))
{throw new XPathException(this, ErrorCodes.XPST0080, "cannot convert to " + Type.getTypeName(requiredType));}

if (requiredType == Type.ANY_SIMPLE_TYPE || expression.returnsType() == Type.ANY_SIMPLE_TYPE || requiredType == Type.UNTYPED || expression.returnsType() == Type.UNTYPED)
if (expression.returnsType() == Type.ANY_SIMPLE_TYPE || requiredType == Type.UNTYPED || expression.returnsType() == Type.UNTYPED)
{throw new XPathException(this, ErrorCodes.XPST0051, "cannot convert to " + Type.getTypeName(requiredType));}

Sequence result;
Expand All @@ -118,13 +119,19 @@ public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathExc
}
else {
try {
// Per XPath F&O 3.1: if the cast is impossible per the casting table,
// castable as returns false without attempting the conversion.
if (!Type.isCastable(seq.itemAt(0).getType(), requiredType)) {
result = BooleanValue.FALSE;
} else {
seq.itemAt(0).convertTo(requiredType);
//If ? is specified after the target type, the result of the cast expression is an empty sequence.
if (requiredCardinality.isSuperCardinalityOrEqualOf(seq.getCardinality()))
{result = BooleanValue.TRUE;}
//If ? is not specified after the target type, a type error is raised [err:XPTY0004].
else
{result = BooleanValue.FALSE;}
}
//TODO : improve by *not* using a costly exception ?
} catch(final XPathException e) {
result = BooleanValue.FALSE;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathExc
while(next != null) {
context.proceed(this, builder);
if (next.getType() == Type.ATTRIBUTE || next.getType() == Type.NAMESPACE)
{throw new XPathException(this, "Found a node of type " +
{throw new XPathException(this, ErrorCodes.XPTY0004, "Found a node of type " +
Type.getTypeName(next.getType()) + " inside a document constructor");}
// if item is an atomic value, collect the string values of all
// following atomic values and seperate them by a space.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathExc

final Sequence nameSeq = qnameExpr.eval(contextSequence, contextItem);
if(!nameSeq.hasOne())
{throw new XPathException(this, "The name expression should evaluate to a single value");}
{throw new XPathException(this, ErrorCodes.XPTY0004, "The name expression should evaluate to a single value");}

final Item qnItem = nameSeq.itemAt(0);
QName qn;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ public DynamicCardinalityCheck(final XQueryContext context, final Cardinality re
setLocation(expression.getLine(), expression.getColumn());
}

public Expression getExpression() {
return expression;
}

/* (non-Javadoc)
* @see org.exist.xquery.Expression#analyze(org.exist.xquery.Expression)
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,14 @@ private static CastExpression castExpression(XQueryContext context,
ErrorCodes.XPST0017, "Wrong number of arguments for constructor function");
}
final Expression arg = params.getFirst();
final int code = Type.getType(qname);
final int code;
try {
code = Type.getType(qname);
} catch (final XPathException e) {
// Unknown type name in xs: namespace → XPST0017 (no such function)
throw new XPathException(ast.getLine(), ast.getColumn(),
ErrorCodes.XPST0017, "Unknown constructor function: " + qname.getStringValue());
}
final CastExpression castExpr = new CastExpression(context, arg, code, Cardinality.ZERO_OR_ONE);
castExpr.setLocation(ast.getLine(), ast.getColumn());
return castExpr;
Expand Down
17 changes: 16 additions & 1 deletion exist-core/src/main/java/org/exist/xquery/OpSimpleMap.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,22 @@ public OpSimpleMap(XQueryContext context, PathExpr left, PathExpr right) {
@Override
public void analyze(AnalyzeContextInfo contextInfo) throws XPathException {
left.analyze(new AnalyzeContextInfo(contextInfo));
right.analyze(new AnalyzeContextInfo(contextInfo));
// The right side of "!" evaluates with each left-side item as the new
// context item. Strip IN_PREDICATE so that sub-expressions (e.g.
// GeneralComparison) do not apply the node-set-intersection optimisation
// that is only valid for the outer predicate's direct children.
final AnalyzeContextInfo rightContextInfo = new AnalyzeContextInfo(contextInfo);
rightContextInfo.removeFlag(IN_PREDICATE);
right.analyze(rightContextInfo);
}

@Override
public int getDependencies() {
// The simple map's external dependencies come from the left operand,
// which evaluates in the caller's context. The right operand evaluates
// in a per-item context derived from the left, so its dependencies are
// internal to the operator.
return left.getDependencies();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -715,13 +715,19 @@ private String format(final NumericValue number, final DecimalFormat decimalForm
if (minimumExponentSize > 0) {
formatted.append(decimalFormat.exponentSeparator);

final CodePointString expStr = new CodePointString(String.valueOf(exp));
// Handle negative exponents: pad the absolute value, then prepend sign
final boolean negativeExp = exp < 0;
final CodePointString expStr = new CodePointString(String.valueOf(Math.abs(exp)));

final int expPadLen = subPicture.getMinimumExponentSize() - expStr.length();
if (expPadLen > 0) {
expStr.leftPad(decimalFormat.zeroDigit, expPadLen);
}

if (negativeExp) {
expStr.insert(0, decimalFormat.minusSign);
}

formatted.append(expStr);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ private static int compareContents(Node a, Node b, @Nullable final Collator coll
}
break;
default:
throw new RuntimeException("unexpected node type " + nodeTypeA);
break;
}
a = findNextTextOrElementNode(a.getNextSibling());
b = findNextTextOrElementNode(b.getNextSibling());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ public Sequence eval(Sequence[] args, Sequence contextSequence) throws XPathExce
final ValueSequence result = new ValueSequence();

for (final String prefix : prefixes.keySet()) {
// Per XQuery spec §14.2: "xmlns" must not be included in the result
if ("xmlns".equals(prefix)) {
continue;
}
//The predefined namespaces (e.g. "exist" for temporary nodes) could have been removed from the static context
if (!(context.getURIForPrefix(prefix) == null &&
("exist".equals(prefix) || "xs".equals(prefix) || "xsi".equals(prefix) ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,9 @@ public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathExc
{max = value;}

else {
if (Type.getCommonSuperType(max.getType(), value.getType()) == Type.ANY_ATOMIC_TYPE) {
if (Type.getCommonSuperType(max.getType(), value.getType()) == Type.ANY_ATOMIC_TYPE
&& !(Type.subTypeOfUnion(max.getType(), Type.NUMERIC)
&& Type.subTypeOfUnion(value.getType(), Type.NUMERIC))) {
throw new XPathException(this, ErrorCodes.FORG0006, "Cannot compare " + Type.getTypeName(max.getType()) +
" and " + Type.getTypeName(value.getType()), max);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,9 @@ public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathExc
if (min == null)
{min = value;}
else {
if (Type.getCommonSuperType(min.getType(), value.getType()) == Type.ANY_ATOMIC_TYPE) {
if (Type.getCommonSuperType(min.getType(), value.getType()) == Type.ANY_ATOMIC_TYPE
&& !(Type.subTypeOfUnion(min.getType(), Type.NUMERIC)
&& Type.subTypeOfUnion(value.getType(), Type.NUMERIC))) {
throw new XPathException(this, ErrorCodes.FORG0006, "Cannot compare " + Type.getTypeName(min.getType()) +
" and " + Type.getTypeName(value.getType()), value);
}
Expand Down
Loading