diff --git a/exist-distribution/src/main/config/conf.xml b/exist-distribution/src/main/config/conf.xml
index 026734ebf05..c4f1f037dea 100644
--- a/exist-distribution/src/main/config/conf.xml
+++ b/exist-distribution/src/main/config/conf.xml
@@ -1009,6 +1009,7 @@
+
diff --git a/extensions/expath/src/main/java/org/expath/exist/BinaryBasicFunctions.java b/extensions/expath/src/main/java/org/expath/exist/BinaryBasicFunctions.java
new file mode 100644
index 00000000000..c96b65bfe6d
--- /dev/null
+++ b/extensions/expath/src/main/java/org/expath/exist/BinaryBasicFunctions.java
@@ -0,0 +1,326 @@
+/*
+ * eXist-db Open Source Native XML Database
+ * Copyright (C) 2001 The eXist-db Authors
+ *
+ * info@exist-db.org
+ * http://www.exist-db.org
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+package org.expath.exist;
+
+import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream;
+import org.exist.dom.QName;
+import org.exist.xquery.BasicFunction;
+import org.exist.xquery.FunctionSignature;
+import org.exist.xquery.XPathException;
+import org.exist.xquery.XQueryContext;
+import org.exist.xquery.value.IntegerValue;
+import org.exist.xquery.value.Sequence;
+import org.exist.xquery.value.Type;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import static org.exist.xquery.FunctionDSL.*;
+
+/**
+ * EXPath Binary Module 4.0 — Basic Operations (Section 5).
+ *
+ *
+ * - bin:length
+ * - bin:part
+ * - bin:join
+ * - bin:insert-before
+ * - bin:pad-left
+ * - bin:pad-right
+ * - bin:find
+ *
+ *
+ * @see EXPath Binary Module 4.0 §5
+ */
+public class BinaryBasicFunctions extends BasicFunction {
+
+ private static final QName QN_LENGTH = new QName("length", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX);
+ private static final QName QN_PART = new QName("part", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX);
+ private static final QName QN_JOIN = new QName("join", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX);
+ private static final QName QN_INSERT_BEFORE = new QName("insert-before", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX);
+ private static final QName QN_PAD_LEFT = new QName("pad-left", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX);
+ private static final QName QN_PAD_RIGHT = new QName("pad-right", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX);
+ private static final QName QN_FIND = new QName("find", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX);
+
+ static final FunctionSignature FS_LENGTH = functionSignature(
+ QN_LENGTH,
+ "Returns the size of binary data in octets.",
+ returns(Type.INTEGER),
+ param("value", Type.BASE64_BINARY, "The binary data")
+ );
+
+ static final FunctionSignature[] FS_PART = functionSignatures(
+ QN_PART,
+ "Returns a specified part of binary data.",
+ returnsOpt(Type.BASE64_BINARY),
+ arities(
+ arity(
+ optParam("value", Type.BASE64_BINARY, "The binary data"),
+ param("offset", Type.INTEGER, "The zero-based offset")
+ ),
+ arity(
+ optParam("value", Type.BASE64_BINARY, "The binary data"),
+ param("offset", Type.INTEGER, "The zero-based offset"),
+ param("size", Type.INTEGER, "The number of octets to return")
+ )
+ )
+ );
+
+ static final FunctionSignature FS_JOIN = functionSignature(
+ QN_JOIN,
+ "Returns the concatenation of binary data.",
+ returns(Type.BASE64_BINARY),
+ optManyParam("values", Type.BASE64_BINARY, "The binary data items to join")
+ );
+
+ static final FunctionSignature FS_INSERT_BEFORE = functionSignature(
+ QN_INSERT_BEFORE,
+ "Inserts additional binary data at a given point in other binary data.",
+ returnsOpt(Type.BASE64_BINARY),
+ optParam("value", Type.BASE64_BINARY, "The binary data"),
+ param("offset", Type.INTEGER, "The zero-based offset for insertion"),
+ optParam("extra", Type.BASE64_BINARY, "The binary data to insert")
+ );
+
+ static final FunctionSignature[] FS_PAD_LEFT = functionSignatures(
+ QN_PAD_LEFT,
+ "Pads binary data on the left to a specified size.",
+ returnsOpt(Type.BASE64_BINARY),
+ arities(
+ arity(
+ optParam("value", Type.BASE64_BINARY, "The binary data"),
+ param("count", Type.INTEGER, "The number of octets to pad")
+ ),
+ arity(
+ optParam("value", Type.BASE64_BINARY, "The binary data"),
+ param("count", Type.INTEGER, "The number of octets to pad"),
+ param("octet", Type.INTEGER, "The octet value to use for padding (0-255)")
+ )
+ )
+ );
+
+ static final FunctionSignature[] FS_PAD_RIGHT = functionSignatures(
+ QN_PAD_RIGHT,
+ "Pads binary data on the right to a specified size.",
+ returnsOpt(Type.BASE64_BINARY),
+ arities(
+ arity(
+ optParam("value", Type.BASE64_BINARY, "The binary data"),
+ param("count", Type.INTEGER, "The number of octets to pad")
+ ),
+ arity(
+ optParam("value", Type.BASE64_BINARY, "The binary data"),
+ param("count", Type.INTEGER, "The number of octets to pad"),
+ param("octet", Type.INTEGER, "The octet value to use for padding (0-255)")
+ )
+ )
+ );
+
+ static final FunctionSignature FS_FIND = functionSignature(
+ QN_FIND,
+ "Returns the first location of a binary search sequence within binary data.",
+ returnsOpt(Type.INTEGER),
+ optParam("value", Type.BASE64_BINARY, "The binary data to search"),
+ param("offset", Type.INTEGER, "The zero-based offset to start searching from"),
+ param("search", Type.BASE64_BINARY, "The binary data to search for")
+ );
+
+ public BinaryBasicFunctions(final XQueryContext context, final FunctionSignature signature) {
+ super(context, signature);
+ }
+
+ @Override
+ public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException {
+ if (isCalledAs("length")) {
+ return length(args);
+ } else if (isCalledAs("part")) {
+ return part(args);
+ } else if (isCalledAs("join")) {
+ return join(args);
+ } else if (isCalledAs("insert-before")) {
+ return insertBefore(args);
+ } else if (isCalledAs("pad-left")) {
+ return padLeft(args);
+ } else if (isCalledAs("pad-right")) {
+ return padRight(args);
+ } else {
+ return find(args);
+ }
+ }
+
+ private Sequence length(final Sequence[] args) throws XPathException {
+ final byte[] data = BinaryModuleHelper.getBinaryData(args[0]);
+ if (data == null) {
+ throw new XPathException(this, org.exist.xquery.ErrorCodes.XPTY0004,
+ "Empty sequence is not allowed as argument to bin:length()");
+ }
+ return new IntegerValue(this, data.length);
+ }
+
+ private Sequence part(final Sequence[] args) throws XPathException {
+ final byte[] data = BinaryModuleHelper.getBinaryData(args[0]);
+ if (data == null) {
+ return Sequence.EMPTY_SEQUENCE;
+ }
+
+ final int offset = ((IntegerValue) args[1].itemAt(0)).getInt();
+ if (offset < 0 || offset > data.length) {
+ throw new XPathException(this, BinaryModuleErrorCode.INDEX_OUT_OF_RANGE,
+ "Offset " + offset + " is out of range for binary data of length " + data.length);
+ }
+
+ final int size;
+ if (args.length > 2 && !args[2].isEmpty()) {
+ size = ((IntegerValue) args[2].itemAt(0)).getInt();
+ if (size < 0) {
+ throw new XPathException(this, BinaryModuleErrorCode.NEGATIVE_SIZE,
+ "Size must not be negative: " + size);
+ }
+ if (offset + size > data.length) {
+ throw new XPathException(this, BinaryModuleErrorCode.INDEX_OUT_OF_RANGE,
+ "Offset " + offset + " + size " + size + " exceeds binary data length " + data.length);
+ }
+ } else {
+ size = data.length - offset;
+ }
+
+ final byte[] result = Arrays.copyOfRange(data, offset, offset + size);
+ return BinaryModuleHelper.createBinaryResult(context, this, result);
+ }
+
+ private Sequence join(final Sequence[] args) throws XPathException {
+ if (args[0].isEmpty()) {
+ return BinaryModuleHelper.createBinaryResult(context, this, new byte[0]);
+ }
+
+ try (final UnsynchronizedByteArrayOutputStream os = new UnsynchronizedByteArrayOutputStream()) {
+ for (int i = 0; i < args[0].getItemCount(); i++) {
+ final byte[] data = BinaryModuleHelper.getBinaryData(args[0].itemAt(i).toSequence());
+ if (data != null) {
+ os.write(data);
+ }
+ }
+ return BinaryModuleHelper.createBinaryResult(context, this, os.toByteArray());
+ } catch (final IOException e) {
+ throw new XPathException(this, "Failed to join binary data: " + e.getMessage(), e);
+ }
+ }
+
+ private Sequence insertBefore(final Sequence[] args) throws XPathException {
+ final byte[] data = BinaryModuleHelper.getBinaryData(args[0]);
+ if (data == null) {
+ return Sequence.EMPTY_SEQUENCE;
+ }
+
+ final int offset = ((IntegerValue) args[1].itemAt(0)).getInt();
+ if (offset < 0 || offset > data.length) {
+ throw new XPathException(this, BinaryModuleErrorCode.INDEX_OUT_OF_RANGE,
+ "Offset " + offset + " is out of range for binary data of length " + data.length);
+ }
+
+ final byte[] extra = BinaryModuleHelper.getBinaryData(args[2]);
+ if (extra == null || extra.length == 0) {
+ return BinaryModuleHelper.createBinaryResult(context, this, data);
+ }
+
+ final byte[] result = new byte[data.length + extra.length];
+ System.arraycopy(data, 0, result, 0, offset);
+ System.arraycopy(extra, 0, result, offset, extra.length);
+ System.arraycopy(data, offset, result, offset + extra.length, data.length - offset);
+ return BinaryModuleHelper.createBinaryResult(context, this, result);
+ }
+
+ private Sequence padLeft(final Sequence[] args) throws XPathException {
+ final byte[] data = BinaryModuleHelper.getBinaryData(args[0]);
+ if (data == null) {
+ return Sequence.EMPTY_SEQUENCE;
+ }
+
+ final int count = ((IntegerValue) args[1].itemAt(0)).getInt();
+ if (count < 0) {
+ throw new XPathException(this, BinaryModuleErrorCode.NEGATIVE_SIZE,
+ "Pad count must not be negative: " + count);
+ }
+
+ final byte octet = (args.length > 2 && !args[2].isEmpty())
+ ? (byte) ((IntegerValue) args[2].itemAt(0)).getInt()
+ : 0;
+
+ final byte[] result = new byte[data.length + count];
+ Arrays.fill(result, 0, count, octet);
+ System.arraycopy(data, 0, result, count, data.length);
+ return BinaryModuleHelper.createBinaryResult(context, this, result);
+ }
+
+ private Sequence padRight(final Sequence[] args) throws XPathException {
+ final byte[] data = BinaryModuleHelper.getBinaryData(args[0]);
+ if (data == null) {
+ return Sequence.EMPTY_SEQUENCE;
+ }
+
+ final int count = ((IntegerValue) args[1].itemAt(0)).getInt();
+ if (count < 0) {
+ throw new XPathException(this, BinaryModuleErrorCode.NEGATIVE_SIZE,
+ "Pad count must not be negative: " + count);
+ }
+
+ final byte octet = (args.length > 2 && !args[2].isEmpty())
+ ? (byte) ((IntegerValue) args[2].itemAt(0)).getInt()
+ : 0;
+
+ final byte[] result = new byte[data.length + count];
+ System.arraycopy(data, 0, result, 0, data.length);
+ Arrays.fill(result, data.length, result.length, octet);
+ return BinaryModuleHelper.createBinaryResult(context, this, result);
+ }
+
+ private Sequence find(final Sequence[] args) throws XPathException {
+ final byte[] data = BinaryModuleHelper.getBinaryData(args[0]);
+ if (data == null) {
+ return Sequence.EMPTY_SEQUENCE;
+ }
+
+ final int offset = ((IntegerValue) args[1].itemAt(0)).getInt();
+ if (offset < 0 || offset > data.length) {
+ throw new XPathException(this, BinaryModuleErrorCode.INDEX_OUT_OF_RANGE,
+ "Offset " + offset + " is out of range for binary data of length " + data.length);
+ }
+
+ final byte[] search = BinaryModuleHelper.getBinaryData(args[2]);
+ if (search == null || search.length == 0) {
+ return new IntegerValue(this, offset);
+ }
+
+ // Naive byte subsequence search
+ outer:
+ for (int i = offset; i <= data.length - search.length; i++) {
+ for (int j = 0; j < search.length; j++) {
+ if (data[i + j] != search[j]) {
+ continue outer;
+ }
+ }
+ return new IntegerValue(this, i);
+ }
+
+ return Sequence.EMPTY_SEQUENCE;
+ }
+}
diff --git a/extensions/expath/src/main/java/org/expath/exist/BinaryBitwiseFunctions.java b/extensions/expath/src/main/java/org/expath/exist/BinaryBitwiseFunctions.java
new file mode 100644
index 00000000000..a403d851eeb
--- /dev/null
+++ b/extensions/expath/src/main/java/org/expath/exist/BinaryBitwiseFunctions.java
@@ -0,0 +1,199 @@
+/*
+ * eXist-db Open Source Native XML Database
+ * Copyright (C) 2001 The eXist-db Authors
+ *
+ * info@exist-db.org
+ * http://www.exist-db.org
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+package org.expath.exist;
+
+import org.exist.dom.QName;
+import org.exist.xquery.BasicFunction;
+import org.exist.xquery.FunctionSignature;
+import org.exist.xquery.XPathException;
+import org.exist.xquery.XQueryContext;
+import org.exist.xquery.value.IntegerValue;
+import org.exist.xquery.value.Sequence;
+import org.exist.xquery.value.Type;
+
+import java.math.BigInteger;
+import java.util.Arrays;
+
+import static org.exist.xquery.FunctionDSL.*;
+
+/**
+ * EXPath Binary Module 4.0 — Bitwise Operations (Section 8).
+ *
+ *
+ * - bin:or
+ * - bin:xor
+ * - bin:and
+ * - bin:not
+ * - bin:shift
+ *
+ *
+ * @see EXPath Binary Module 4.0 §8
+ */
+public class BinaryBitwiseFunctions extends BasicFunction {
+
+ private static final QName QN_OR = new QName("or", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX);
+ private static final QName QN_XOR = new QName("xor", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX);
+ private static final QName QN_AND = new QName("and", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX);
+ private static final QName QN_NOT = new QName("not", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX);
+ private static final QName QN_SHIFT = new QName("shift", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX);
+
+ static final FunctionSignature FS_OR = functionSignature(
+ QN_OR,
+ "Returns the bitwise OR of two binary values.",
+ returnsOpt(Type.BASE64_BINARY),
+ optParam("value1", Type.BASE64_BINARY, "The first binary value"),
+ optParam("value2", Type.BASE64_BINARY, "The second binary value")
+ );
+
+ static final FunctionSignature FS_XOR = functionSignature(
+ QN_XOR,
+ "Returns the bitwise XOR of two binary values.",
+ returnsOpt(Type.BASE64_BINARY),
+ optParam("value1", Type.BASE64_BINARY, "The first binary value"),
+ optParam("value2", Type.BASE64_BINARY, "The second binary value")
+ );
+
+ static final FunctionSignature FS_AND = functionSignature(
+ QN_AND,
+ "Returns the bitwise AND of two binary values.",
+ returnsOpt(Type.BASE64_BINARY),
+ optParam("value1", Type.BASE64_BINARY, "The first binary value"),
+ optParam("value2", Type.BASE64_BINARY, "The second binary value")
+ );
+
+ static final FunctionSignature FS_NOT = functionSignature(
+ QN_NOT,
+ "Returns the bitwise NOT of a binary value.",
+ returnsOpt(Type.BASE64_BINARY),
+ optParam("value", Type.BASE64_BINARY, "The binary value")
+ );
+
+ static final FunctionSignature FS_SHIFT = functionSignature(
+ QN_SHIFT,
+ "Shifts bits in binary data. Positive shifts left, negative shifts right.",
+ returnsOpt(Type.BASE64_BINARY),
+ optParam("value", Type.BASE64_BINARY, "The binary value"),
+ param("by", Type.INTEGER, "The number of bits to shift (positive = left, negative = right)")
+ );
+
+ public BinaryBitwiseFunctions(final XQueryContext context, final FunctionSignature signature) {
+ super(context, signature);
+ }
+
+ @Override
+ public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException {
+ if (isCalledAs("or")) {
+ return bitwiseOp(args, BitwiseOp.OR);
+ } else if (isCalledAs("xor")) {
+ return bitwiseOp(args, BitwiseOp.XOR);
+ } else if (isCalledAs("and")) {
+ return bitwiseOp(args, BitwiseOp.AND);
+ } else if (isCalledAs("not")) {
+ return bitwiseNot(args);
+ } else {
+ return bitwiseShift(args);
+ }
+ }
+
+ private enum BitwiseOp { OR, XOR, AND }
+
+ private Sequence bitwiseOp(final Sequence[] args, final BitwiseOp op) throws XPathException {
+ final byte[] data1 = BinaryModuleHelper.getBinaryData(args[0]);
+ final byte[] data2 = BinaryModuleHelper.getBinaryData(args[1]);
+
+ if (data1 == null || data2 == null) {
+ return Sequence.EMPTY_SEQUENCE;
+ }
+
+ if (data1.length != data2.length) {
+ throw new XPathException(this, BinaryModuleErrorCode.DIFFERING_LENGTH_ARGUMENTS,
+ "Arguments to bin:" + op.name().toLowerCase() + "() must have equal length, but got "
+ + data1.length + " and " + data2.length);
+ }
+
+ final byte[] result = new byte[data1.length];
+ for (int i = 0; i < data1.length; i++) {
+ switch (op) {
+ case OR:
+ result[i] = (byte) (data1[i] | data2[i]);
+ break;
+ case XOR:
+ result[i] = (byte) (data1[i] ^ data2[i]);
+ break;
+ case AND:
+ result[i] = (byte) (data1[i] & data2[i]);
+ break;
+ }
+ }
+ return BinaryModuleHelper.createBinaryResult(context, this, result);
+ }
+
+ private Sequence bitwiseNot(final Sequence[] args) throws XPathException {
+ final byte[] data = BinaryModuleHelper.getBinaryData(args[0]);
+ if (data == null) {
+ return Sequence.EMPTY_SEQUENCE;
+ }
+
+ final byte[] result = new byte[data.length];
+ for (int i = 0; i < data.length; i++) {
+ result[i] = (byte) ~data[i];
+ }
+ return BinaryModuleHelper.createBinaryResult(context, this, result);
+ }
+
+ private Sequence bitwiseShift(final Sequence[] args) throws XPathException {
+ final byte[] data = BinaryModuleHelper.getBinaryData(args[0]);
+ if (data == null) {
+ return Sequence.EMPTY_SEQUENCE;
+ }
+
+ final int by = ((IntegerValue) args[1].itemAt(0)).getInt();
+ final int originalLength = data.length;
+
+ if (originalLength == 0) {
+ return BinaryModuleHelper.createBinaryResult(context, this, new byte[0]);
+ }
+
+ // Use BigInteger for bit shifting, then maintain original length
+ BigInteger bigInt = new BigInteger(1, data);
+
+ if (by > 0) {
+ bigInt = bigInt.shiftLeft(by);
+ } else if (by < 0) {
+ bigInt = bigInt.shiftRight(-by);
+ }
+
+ // Convert back to byte array of original length
+ final byte[] shifted = bigInt.toByteArray();
+ final byte[] result = new byte[originalLength];
+
+ if (shifted.length <= originalLength) {
+ // Right-align in result
+ System.arraycopy(shifted, 0, result, originalLength - shifted.length, shifted.length);
+ } else {
+ // Truncate from the left to maintain original length
+ System.arraycopy(shifted, shifted.length - originalLength, result, 0, originalLength);
+ }
+
+ return BinaryModuleHelper.createBinaryResult(context, this, result);
+ }
+}
diff --git a/extensions/expath/src/main/java/org/expath/exist/BinaryConversionFunctions.java b/extensions/expath/src/main/java/org/expath/exist/BinaryConversionFunctions.java
new file mode 100644
index 00000000000..8538eddf710
--- /dev/null
+++ b/extensions/expath/src/main/java/org/expath/exist/BinaryConversionFunctions.java
@@ -0,0 +1,258 @@
+/*
+ * eXist-db Open Source Native XML Database
+ * Copyright (C) 2001 The eXist-db Authors
+ *
+ * info@exist-db.org
+ * http://www.exist-db.org
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+package org.expath.exist;
+
+import org.exist.dom.QName;
+import org.exist.xquery.BasicFunction;
+import org.exist.xquery.FunctionSignature;
+import org.exist.xquery.XPathException;
+import org.exist.xquery.XQueryContext;
+import org.exist.xquery.value.IntegerValue;
+import org.exist.xquery.value.Sequence;
+import org.exist.xquery.value.Type;
+import org.exist.xquery.value.ValueSequence;
+
+import static org.exist.xquery.FunctionDSL.*;
+
+/**
+ * EXPath Binary Module 4.0 — Constants and Conversions (Section 4).
+ *
+ *
+ * - bin:hex
+ * - bin:bin
+ * - bin:octal
+ * - bin:to-octets
+ * - bin:from-octets
+ *
+ *
+ * @see EXPath Binary Module 4.0 §4
+ */
+public class BinaryConversionFunctions extends BasicFunction {
+
+ private static final QName QN_HEX = new QName("hex", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX);
+ private static final QName QN_BIN = new QName("bin", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX);
+ private static final QName QN_OCTAL = new QName("octal", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX);
+ private static final QName QN_TO_OCTETS = new QName("to-octets", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX);
+ private static final QName QN_FROM_OCTETS = new QName("from-octets", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX);
+
+ static final FunctionSignature FS_HEX = functionSignature(
+ QN_HEX,
+ "Creates an xs:base64Binary value from a hexadecimal string.",
+ returnsOpt(Type.BASE64_BINARY),
+ optParam("value", Type.STRING, "The hexadecimal string")
+ );
+
+ static final FunctionSignature FS_BIN = functionSignature(
+ QN_BIN,
+ "Creates an xs:base64Binary value from a binary (0/1) string.",
+ returnsOpt(Type.BASE64_BINARY),
+ optParam("value", Type.STRING, "The binary digit string")
+ );
+
+ static final FunctionSignature FS_OCTAL = functionSignature(
+ QN_OCTAL,
+ "Creates an xs:base64Binary value from an octal string.",
+ returnsOpt(Type.BASE64_BINARY),
+ optParam("value", Type.STRING, "The octal string")
+ );
+
+ static final FunctionSignature FS_TO_OCTETS = functionSignature(
+ QN_TO_OCTETS,
+ "Returns the binary data as a sequence of octets.",
+ returnsOptMany(Type.INTEGER),
+ param("value", Type.BASE64_BINARY, "The binary data")
+ );
+
+ static final FunctionSignature FS_FROM_OCTETS = functionSignature(
+ QN_FROM_OCTETS,
+ "Converts a sequence of octets into binary data.",
+ returns(Type.BASE64_BINARY),
+ optManyParam("values", Type.INTEGER, "The octet values (0-255)")
+ );
+
+ public BinaryConversionFunctions(final XQueryContext context, final FunctionSignature signature) {
+ super(context, signature);
+ }
+
+ @Override
+ public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException {
+ if (isCalledAs("hex")) {
+ return hexToBinary(args);
+ } else if (isCalledAs("bin")) {
+ return binToBinary(args);
+ } else if (isCalledAs("octal")) {
+ return octalToBinary(args);
+ } else if (isCalledAs("to-octets")) {
+ return toOctets(args);
+ } else {
+ return fromOctets(args);
+ }
+ }
+
+ private Sequence hexToBinary(final Sequence[] args) throws XPathException {
+ if (args[0].isEmpty()) {
+ return Sequence.EMPTY_SEQUENCE;
+ }
+
+ String hex = args[0].getStringValue();
+ // Strip whitespace and underscores per spec
+ hex = hex.replaceAll("[\\s_]", "");
+
+ if (hex.isEmpty()) {
+ return BinaryModuleHelper.createBinaryResult(context, this, new byte[0]);
+ }
+
+ // Validate characters
+ for (int i = 0; i < hex.length(); i++) {
+ final char c = hex.charAt(i);
+ if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
+ throw new XPathException(this, BinaryModuleErrorCode.NON_NUMERIC_CHARACTER,
+ "Invalid hexadecimal character: '" + c + "'");
+ }
+ }
+
+ // Prepend "0" if odd length
+ if (hex.length() % 2 != 0) {
+ hex = "0" + hex;
+ }
+
+ final byte[] data = new byte[hex.length() / 2];
+ for (int i = 0; i < data.length; i++) {
+ data[i] = (byte) Integer.parseInt(hex.substring(i * 2, i * 2 + 2), 16);
+ }
+ return BinaryModuleHelper.createBinaryResult(context, this, data);
+ }
+
+ private Sequence binToBinary(final Sequence[] args) throws XPathException {
+ if (args[0].isEmpty()) {
+ return Sequence.EMPTY_SEQUENCE;
+ }
+
+ String bin = args[0].getStringValue();
+ bin = bin.replaceAll("[\\s_]", "");
+
+ if (bin.isEmpty()) {
+ return BinaryModuleHelper.createBinaryResult(context, this, new byte[0]);
+ }
+
+ // Validate characters
+ for (int i = 0; i < bin.length(); i++) {
+ final char c = bin.charAt(i);
+ if (c != '0' && c != '1') {
+ throw new XPathException(this, BinaryModuleErrorCode.NON_NUMERIC_CHARACTER,
+ "Invalid binary character: '" + c + "'");
+ }
+ }
+
+ // Pad to 8-bit multiple
+ final int remainder = bin.length() % 8;
+ if (remainder != 0) {
+ bin = "0".repeat(8 - remainder) + bin;
+ }
+
+ final byte[] data = new byte[bin.length() / 8];
+ for (int i = 0; i < data.length; i++) {
+ data[i] = (byte) Integer.parseInt(bin.substring(i * 8, i * 8 + 8), 2);
+ }
+ return BinaryModuleHelper.createBinaryResult(context, this, data);
+ }
+
+ private Sequence octalToBinary(final Sequence[] args) throws XPathException {
+ if (args[0].isEmpty()) {
+ return Sequence.EMPTY_SEQUENCE;
+ }
+
+ String octal = args[0].getStringValue();
+ octal = octal.replaceAll("[\\s_]", "");
+
+ if (octal.isEmpty()) {
+ return BinaryModuleHelper.createBinaryResult(context, this, new byte[0]);
+ }
+
+ // Validate characters
+ for (int i = 0; i < octal.length(); i++) {
+ final char c = octal.charAt(i);
+ if (c < '0' || c > '7') {
+ throw new XPathException(this, BinaryModuleErrorCode.NON_NUMERIC_CHARACTER,
+ "Invalid octal character: '" + c + "'");
+ }
+ }
+
+ // Convert each octal digit to 3-bit binary
+ final StringBuilder bits = new StringBuilder();
+ for (int i = 0; i < octal.length(); i++) {
+ final int digit = octal.charAt(i) - '0';
+ bits.append(String.format("%3s", Integer.toBinaryString(digit)).replace(' ', '0'));
+ }
+
+ // Strip up to 2 leading zeros (octal digit = 3 bits, but only multiples of 8 matter)
+ String binaryStr = bits.toString();
+ int stripCount = 0;
+ while (stripCount < 2 && binaryStr.length() > 0 && binaryStr.charAt(0) == '0'
+ && (binaryStr.length() - 1) % 8 != 7) {
+ binaryStr = binaryStr.substring(1);
+ stripCount++;
+ }
+
+ // Pad to 8-bit multiple
+ final int remainder = binaryStr.length() % 8;
+ if (remainder != 0) {
+ binaryStr = "0".repeat(8 - remainder) + binaryStr;
+ }
+
+ if (binaryStr.isEmpty()) {
+ return BinaryModuleHelper.createBinaryResult(context, this, new byte[0]);
+ }
+
+ final byte[] data = new byte[binaryStr.length() / 8];
+ for (int i = 0; i < data.length; i++) {
+ data[i] = (byte) Integer.parseInt(binaryStr.substring(i * 8, i * 8 + 8), 2);
+ }
+ return BinaryModuleHelper.createBinaryResult(context, this, data);
+ }
+
+ private Sequence toOctets(final Sequence[] args) throws XPathException {
+ final byte[] data = BinaryModuleHelper.getBinaryData(args[0]);
+ if (data == null || data.length == 0) {
+ return Sequence.EMPTY_SEQUENCE;
+ }
+
+ final ValueSequence result = new ValueSequence(data.length);
+ for (final byte b : data) {
+ result.add(new IntegerValue(this, b & 0xFF));
+ }
+ return result;
+ }
+
+ private Sequence fromOctets(final Sequence[] args) throws XPathException {
+ if (args[0].isEmpty()) {
+ return BinaryModuleHelper.createBinaryResult(context, this, new byte[0]);
+ }
+
+ final int len = args[0].getItemCount();
+ final byte[] data = new byte[len];
+ for (int i = 0; i < len; i++) {
+ data[i] = (byte) ((IntegerValue) args[0].itemAt(i)).getInt();
+ }
+ return BinaryModuleHelper.createBinaryResult(context, this, data);
+ }
+}
diff --git a/extensions/expath/src/main/java/org/expath/exist/BinaryModule.java b/extensions/expath/src/main/java/org/expath/exist/BinaryModule.java
new file mode 100644
index 00000000000..2d5a2970e47
--- /dev/null
+++ b/extensions/expath/src/main/java/org/expath/exist/BinaryModule.java
@@ -0,0 +1,119 @@
+/*
+ * eXist-db Open Source Native XML Database
+ * Copyright (C) 2001 The eXist-db Authors
+ *
+ * info@exist-db.org
+ * http://www.exist-db.org
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+package org.expath.exist;
+
+import org.exist.xquery.AbstractInternalModule;
+import org.exist.xquery.FunctionDef;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.exist.xquery.FunctionDSL.functionDefs;
+
+/**
+ * EXPath Binary Module 4.0.
+ *
+ * @see EXPath Binary Module 4.0
+ */
+public class BinaryModule extends AbstractInternalModule {
+
+ public static final String NAMESPACE_URI = "http://expath.org/ns/binary";
+ public static final String PREFIX = "bin";
+ public static final String INCLUSION_DATE = "2026-03-04";
+ public static final String RELEASED_IN_VERSION = "7.0.0";
+
+ private static final FunctionDef[] functions = functionDefs(
+ functionDefs(BinaryConversionFunctions.class,
+ BinaryConversionFunctions.FS_HEX,
+ BinaryConversionFunctions.FS_BIN,
+ BinaryConversionFunctions.FS_OCTAL,
+ BinaryConversionFunctions.FS_TO_OCTETS,
+ BinaryConversionFunctions.FS_FROM_OCTETS),
+
+ functionDefs(BinaryBasicFunctions.class,
+ BinaryBasicFunctions.FS_LENGTH,
+ BinaryBasicFunctions.FS_PART[0],
+ BinaryBasicFunctions.FS_PART[1],
+ BinaryBasicFunctions.FS_JOIN,
+ BinaryBasicFunctions.FS_INSERT_BEFORE,
+ BinaryBasicFunctions.FS_PAD_LEFT[0],
+ BinaryBasicFunctions.FS_PAD_LEFT[1],
+ BinaryBasicFunctions.FS_PAD_RIGHT[0],
+ BinaryBasicFunctions.FS_PAD_RIGHT[1],
+ BinaryBasicFunctions.FS_FIND),
+
+ functionDefs(BinaryTextFunctions.class,
+ BinaryTextFunctions.FS_DECODE_STRING[0],
+ BinaryTextFunctions.FS_DECODE_STRING[1],
+ BinaryTextFunctions.FS_DECODE_STRING[2],
+ BinaryTextFunctions.FS_DECODE_STRING[3],
+ BinaryTextFunctions.FS_ENCODE_STRING[0],
+ BinaryTextFunctions.FS_ENCODE_STRING[1]),
+
+ functionDefs(BinaryPackingFunctions.class,
+ BinaryPackingFunctions.FS_PACK_DOUBLE[0],
+ BinaryPackingFunctions.FS_PACK_DOUBLE[1],
+ BinaryPackingFunctions.FS_PACK_FLOAT[0],
+ BinaryPackingFunctions.FS_PACK_FLOAT[1],
+ BinaryPackingFunctions.FS_PACK_INTEGER[0],
+ BinaryPackingFunctions.FS_PACK_INTEGER[1],
+ BinaryPackingFunctions.FS_UNPACK_DOUBLE[0],
+ BinaryPackingFunctions.FS_UNPACK_DOUBLE[1],
+ BinaryPackingFunctions.FS_UNPACK_FLOAT[0],
+ BinaryPackingFunctions.FS_UNPACK_FLOAT[1],
+ BinaryPackingFunctions.FS_UNPACK_INTEGER[0],
+ BinaryPackingFunctions.FS_UNPACK_INTEGER[1],
+ BinaryPackingFunctions.FS_UNPACK_UNSIGNED_INTEGER[0],
+ BinaryPackingFunctions.FS_UNPACK_UNSIGNED_INTEGER[1]),
+
+ functionDefs(BinaryBitwiseFunctions.class,
+ BinaryBitwiseFunctions.FS_OR,
+ BinaryBitwiseFunctions.FS_XOR,
+ BinaryBitwiseFunctions.FS_AND,
+ BinaryBitwiseFunctions.FS_NOT,
+ BinaryBitwiseFunctions.FS_SHIFT)
+ );
+
+ public BinaryModule(final Map> parameters) {
+ super(functions, parameters);
+ }
+
+ @Override
+ public String getNamespaceURI() {
+ return NAMESPACE_URI;
+ }
+
+ @Override
+ public String getDefaultPrefix() {
+ return PREFIX;
+ }
+
+ @Override
+ public String getDescription() {
+ return "EXPath Binary Module 4.0 https://qt4cg.org/specifications/expath-binary-40/Overview.html";
+ }
+
+ @Override
+ public String getReleaseVersion() {
+ return RELEASED_IN_VERSION;
+ }
+}
diff --git a/extensions/expath/src/main/java/org/expath/exist/BinaryModuleErrorCode.java b/extensions/expath/src/main/java/org/expath/exist/BinaryModuleErrorCode.java
new file mode 100644
index 00000000000..9422a24e65c
--- /dev/null
+++ b/extensions/expath/src/main/java/org/expath/exist/BinaryModuleErrorCode.java
@@ -0,0 +1,68 @@
+/*
+ * eXist-db Open Source Native XML Database
+ * Copyright (C) 2001 The eXist-db Authors
+ *
+ * info@exist-db.org
+ * http://www.exist-db.org
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+package org.expath.exist;
+
+import org.exist.dom.QName;
+import org.exist.xquery.ErrorCodes.ErrorCode;
+
+/**
+ * Error codes for the EXPath Binary Module 4.0.
+ *
+ * @see EXPath Binary Module 4.0 - Errors
+ */
+public class BinaryModuleErrorCode {
+
+ public static final ErrorCode NON_NUMERIC_CHARACTER = new ErrorCode(
+ new QName("non-numeric-character", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX),
+ "The argument to bin:hex(), bin:bin(), or bin:octal() contains a character that is not valid for the specified notation.");
+
+ public static final ErrorCode INDEX_OUT_OF_RANGE = new ErrorCode(
+ new QName("index-out-of-range", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX),
+ "Offset and/or size is out of range for the given binary data.");
+
+ public static final ErrorCode NEGATIVE_SIZE = new ErrorCode(
+ new QName("negative-size", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX),
+ "Size, count, or padding is negative.");
+
+ public static final ErrorCode UNKNOWN_ENCODING = new ErrorCode(
+ new QName("unknown-encoding", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX),
+ "The specified encoding is not supported.");
+
+ public static final ErrorCode CONVERSION_ERROR = new ErrorCode(
+ new QName("conversion-error", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX),
+ "An error occurred during encoding or decoding of a string.");
+
+ public static final ErrorCode DIFFERING_LENGTH_ARGUMENTS = new ErrorCode(
+ new QName("differing-length-arguments", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX),
+ "The arguments to a bitwise operation are of differing length.");
+
+ public static final ErrorCode INVALID_ENCODING = new ErrorCode(
+ new QName("invalid-encoding", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX),
+ "The encoding is invalid for the given data.");
+
+ public static final ErrorCode INTEGER_TOO_LARGE = new ErrorCode(
+ new QName("integer-too-large", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX),
+ "Integer value exceeds the implementation-defined maximum.");
+
+ private BinaryModuleErrorCode() {
+ }
+}
diff --git a/extensions/expath/src/main/java/org/expath/exist/BinaryModuleHelper.java b/extensions/expath/src/main/java/org/expath/exist/BinaryModuleHelper.java
new file mode 100644
index 00000000000..700fb97296a
--- /dev/null
+++ b/extensions/expath/src/main/java/org/expath/exist/BinaryModuleHelper.java
@@ -0,0 +1,119 @@
+/*
+ * eXist-db Open Source Native XML Database
+ * Copyright (C) 2001 The eXist-db Authors
+ *
+ * info@exist-db.org
+ * http://www.exist-db.org
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+package org.expath.exist;
+
+import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream;
+import org.exist.xquery.Expression;
+import org.exist.xquery.XPathException;
+import org.exist.xquery.XQueryContext;
+import org.exist.xquery.value.Base64BinaryValueType;
+import org.exist.xquery.value.BinaryValue;
+import org.exist.xquery.value.BinaryValueFromInputStream;
+import org.exist.xquery.value.Sequence;
+
+import org.apache.commons.io.input.UnsynchronizedByteArrayInputStream;
+
+import javax.annotation.Nullable;
+import java.io.IOException;
+
+/**
+ * Shared utility methods for the EXPath Binary Module functions.
+ */
+class BinaryModuleHelper {
+
+ /**
+ * Extracts a byte array from a binary sequence argument.
+ *
+ * @param arg the sequence argument (expected to contain a single binary value)
+ * @return the byte array, or null if the argument is an empty sequence
+ * @throws XPathException if the binary data cannot be read
+ */
+ @Nullable
+ static byte[] getBinaryData(final Sequence arg) throws XPathException {
+ if (arg.isEmpty()) {
+ return null;
+ }
+ final BinaryValue binary = (BinaryValue) arg.itemAt(0);
+ try (final UnsynchronizedByteArrayOutputStream os = new UnsynchronizedByteArrayOutputStream()) {
+ binary.streamBinaryTo(os);
+ return os.toByteArray();
+ } catch (final IOException e) {
+ throw new XPathException((Expression) null, "Failed to read binary data: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Creates an xs:base64Binary value from a byte array.
+ *
+ * @param context the XQuery context
+ * @param expr the calling expression (for error reporting)
+ * @param data the byte array
+ * @return the base64Binary value
+ * @throws XPathException if the value cannot be created
+ */
+ static BinaryValue createBinaryResult(final XQueryContext context, final Expression expr, final byte[] data) throws XPathException {
+ return BinaryValueFromInputStream.getInstance(
+ context,
+ new Base64BinaryValueType(),
+ new UnsynchronizedByteArrayInputStream(data),
+ expr
+ );
+ }
+
+ /**
+ * Validates the octet-order parameter string.
+ *
+ * @param order the order string
+ * @return true if little-endian, false if big-endian
+ * @throws XPathException if the value is not a valid octet order
+ */
+ static boolean isLittleEndian(final Expression expr, final String order) throws XPathException {
+ switch (order) {
+ case "most-significant-first":
+ case "big-endian":
+ case "BE":
+ return false;
+ case "least-significant-first":
+ case "little-endian":
+ case "LE":
+ return true;
+ default:
+ throw new XPathException(expr,
+ org.exist.xquery.ErrorCodes.XPTY0004,
+ "Invalid octet order: '" + order + "'. Expected one of: most-significant-first, big-endian, BE, least-significant-first, little-endian, LE");
+ }
+ }
+
+ /**
+ * Reverses a byte array in place.
+ */
+ static void reverseBytes(final byte[] data) {
+ for (int i = 0, j = data.length - 1; i < j; i++, j--) {
+ final byte tmp = data[i];
+ data[i] = data[j];
+ data[j] = tmp;
+ }
+ }
+
+ private BinaryModuleHelper() {
+ }
+}
diff --git a/extensions/expath/src/main/java/org/expath/exist/BinaryPackingFunctions.java b/extensions/expath/src/main/java/org/expath/exist/BinaryPackingFunctions.java
new file mode 100644
index 00000000000..17e8e273214
--- /dev/null
+++ b/extensions/expath/src/main/java/org/expath/exist/BinaryPackingFunctions.java
@@ -0,0 +1,339 @@
+/*
+ * eXist-db Open Source Native XML Database
+ * Copyright (C) 2001 The eXist-db Authors
+ *
+ * info@exist-db.org
+ * http://www.exist-db.org
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+package org.expath.exist;
+
+import org.exist.dom.QName;
+import org.exist.xquery.BasicFunction;
+import org.exist.xquery.FunctionSignature;
+import org.exist.xquery.XPathException;
+import org.exist.xquery.XQueryContext;
+import org.exist.xquery.value.DoubleValue;
+import org.exist.xquery.value.FloatValue;
+import org.exist.xquery.value.IntegerValue;
+import org.exist.xquery.value.Sequence;
+import org.exist.xquery.value.Type;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+import static org.exist.xquery.FunctionDSL.*;
+
+/**
+ * EXPath Binary Module 4.0 — Numeric Packing and Unpacking (Section 7).
+ *
+ *
+ * - bin:pack-double
+ * - bin:pack-float
+ * - bin:pack-integer
+ * - bin:unpack-double
+ * - bin:unpack-float
+ * - bin:unpack-integer
+ * - bin:unpack-unsigned-integer
+ *
+ *
+ * @see EXPath Binary Module 4.0 §7
+ */
+public class BinaryPackingFunctions extends BasicFunction {
+
+ private static final QName QN_PACK_DOUBLE = new QName("pack-double", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX);
+ private static final QName QN_PACK_FLOAT = new QName("pack-float", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX);
+ private static final QName QN_PACK_INTEGER = new QName("pack-integer", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX);
+ private static final QName QN_UNPACK_DOUBLE = new QName("unpack-double", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX);
+ private static final QName QN_UNPACK_FLOAT = new QName("unpack-float", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX);
+ private static final QName QN_UNPACK_INTEGER = new QName("unpack-integer", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX);
+ private static final QName QN_UNPACK_UNSIGNED_INTEGER = new QName("unpack-unsigned-integer", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX);
+
+ static final FunctionSignature[] FS_PACK_DOUBLE = functionSignatures(
+ QN_PACK_DOUBLE,
+ "Returns the 8-octet binary representation of an xs:double value.",
+ returns(Type.BASE64_BINARY),
+ arities(
+ arity(param("value", Type.DOUBLE, "The double value to pack")),
+ arity(param("value", Type.DOUBLE, "The double value to pack"),
+ param("order", Type.STRING, "The octet order: 'most-significant-first' (default), 'big-endian', 'BE', 'least-significant-first', 'little-endian', 'LE'"))
+ )
+ );
+
+ static final FunctionSignature[] FS_PACK_FLOAT = functionSignatures(
+ QN_PACK_FLOAT,
+ "Returns the 4-octet binary representation of an xs:float value.",
+ returns(Type.BASE64_BINARY),
+ arities(
+ arity(param("value", Type.FLOAT, "The float value to pack")),
+ arity(param("value", Type.FLOAT, "The float value to pack"),
+ param("order", Type.STRING, "The octet order"))
+ )
+ );
+
+ static final FunctionSignature[] FS_PACK_INTEGER = functionSignatures(
+ QN_PACK_INTEGER,
+ "Returns the two's-complement binary representation of an xs:integer value.",
+ returns(Type.BASE64_BINARY),
+ arities(
+ arity(param("value", Type.INTEGER, "The integer value to pack"),
+ param("size", Type.INTEGER, "The number of octets in the result")),
+ arity(param("value", Type.INTEGER, "The integer value to pack"),
+ param("size", Type.INTEGER, "The number of octets in the result"),
+ param("order", Type.STRING, "The octet order"))
+ )
+ );
+
+ static final FunctionSignature[] FS_UNPACK_DOUBLE = functionSignatures(
+ QN_UNPACK_DOUBLE,
+ "Extracts an xs:double value from binary data.",
+ returns(Type.DOUBLE),
+ arities(
+ arity(param("value", Type.BASE64_BINARY, "The binary data"),
+ param("offset", Type.INTEGER, "The zero-based byte offset")),
+ arity(param("value", Type.BASE64_BINARY, "The binary data"),
+ param("offset", Type.INTEGER, "The zero-based byte offset"),
+ param("order", Type.STRING, "The octet order"))
+ )
+ );
+
+ static final FunctionSignature[] FS_UNPACK_FLOAT = functionSignatures(
+ QN_UNPACK_FLOAT,
+ "Extracts an xs:float value from binary data.",
+ returns(Type.FLOAT),
+ arities(
+ arity(param("value", Type.BASE64_BINARY, "The binary data"),
+ param("offset", Type.INTEGER, "The zero-based byte offset")),
+ arity(param("value", Type.BASE64_BINARY, "The binary data"),
+ param("offset", Type.INTEGER, "The zero-based byte offset"),
+ param("order", Type.STRING, "The octet order"))
+ )
+ );
+
+ static final FunctionSignature[] FS_UNPACK_INTEGER = functionSignatures(
+ QN_UNPACK_INTEGER,
+ "Extracts a signed xs:integer value from binary data.",
+ returns(Type.INTEGER),
+ arities(
+ arity(param("value", Type.BASE64_BINARY, "The binary data"),
+ param("offset", Type.INTEGER, "The zero-based byte offset"),
+ param("size", Type.INTEGER, "The number of octets to read")),
+ arity(param("value", Type.BASE64_BINARY, "The binary data"),
+ param("offset", Type.INTEGER, "The zero-based byte offset"),
+ param("size", Type.INTEGER, "The number of octets to read"),
+ param("order", Type.STRING, "The octet order"))
+ )
+ );
+
+ static final FunctionSignature[] FS_UNPACK_UNSIGNED_INTEGER = functionSignatures(
+ QN_UNPACK_UNSIGNED_INTEGER,
+ "Extracts an unsigned xs:integer value from binary data.",
+ returns(Type.INTEGER),
+ arities(
+ arity(param("value", Type.BASE64_BINARY, "The binary data"),
+ param("offset", Type.INTEGER, "The zero-based byte offset"),
+ param("size", Type.INTEGER, "The number of octets to read")),
+ arity(param("value", Type.BASE64_BINARY, "The binary data"),
+ param("offset", Type.INTEGER, "The zero-based byte offset"),
+ param("size", Type.INTEGER, "The number of octets to read"),
+ param("order", Type.STRING, "The octet order"))
+ )
+ );
+
+ public BinaryPackingFunctions(final XQueryContext context, final FunctionSignature signature) {
+ super(context, signature);
+ }
+
+ @Override
+ public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException {
+ if (isCalledAs("pack-double")) {
+ return packDouble(args);
+ } else if (isCalledAs("pack-float")) {
+ return packFloat(args);
+ } else if (isCalledAs("pack-integer")) {
+ return packInteger(args);
+ } else if (isCalledAs("unpack-double")) {
+ return unpackDouble(args);
+ } else if (isCalledAs("unpack-float")) {
+ return unpackFloat(args);
+ } else if (isCalledAs("unpack-integer")) {
+ return unpackInteger(args);
+ } else {
+ return unpackUnsignedInteger(args);
+ }
+ }
+
+ private boolean getByteOrder(final Sequence[] args, final int orderArgIndex) throws XPathException {
+ if (args.length > orderArgIndex && !args[orderArgIndex].isEmpty()) {
+ return BinaryModuleHelper.isLittleEndian(this, args[orderArgIndex].getStringValue());
+ }
+ return false; // big-endian by default
+ }
+
+ private Sequence packDouble(final Sequence[] args) throws XPathException {
+ final double value = ((DoubleValue) args[0].itemAt(0)).getDouble();
+ final boolean le = getByteOrder(args, 1);
+
+ final byte[] data = new byte[8];
+ ByteBuffer.wrap(data).putLong(Double.doubleToRawLongBits(value));
+ if (le) {
+ BinaryModuleHelper.reverseBytes(data);
+ }
+ return BinaryModuleHelper.createBinaryResult(context, this, data);
+ }
+
+ private Sequence packFloat(final Sequence[] args) throws XPathException {
+ final float value = ((FloatValue) args[0].itemAt(0)).getValue();
+ final boolean le = getByteOrder(args, 1);
+
+ final byte[] data = new byte[4];
+ ByteBuffer.wrap(data).putInt(Float.floatToRawIntBits(value));
+ if (le) {
+ BinaryModuleHelper.reverseBytes(data);
+ }
+ return BinaryModuleHelper.createBinaryResult(context, this, data);
+ }
+
+ private Sequence packInteger(final Sequence[] args) throws XPathException {
+ final BigInteger value = ((IntegerValue) args[0].itemAt(0)).toJavaObject(BigInteger.class);
+ final int size = ((IntegerValue) args[1].itemAt(0)).getInt();
+ final boolean le = getByteOrder(args, 2);
+
+ if (size < 0) {
+ throw new XPathException(this, BinaryModuleErrorCode.NEGATIVE_SIZE,
+ "Size must not be negative: " + size);
+ }
+
+ if (size == 0) {
+ return BinaryModuleHelper.createBinaryResult(context, this, new byte[0]);
+ }
+
+ final byte[] twosComplement = value.toByteArray();
+ final byte[] data = new byte[size];
+
+ // Fill with sign extension byte (0x00 for positive, 0xFF for negative)
+ if (value.signum() < 0) {
+ Arrays.fill(data, (byte) 0xFF);
+ }
+
+ // Copy the significant bytes into the result, right-aligned (big-endian)
+ if (twosComplement.length <= size) {
+ System.arraycopy(twosComplement, 0, data, size - twosComplement.length, twosComplement.length);
+ } else {
+ // Truncate from the left (most significant bytes)
+ System.arraycopy(twosComplement, twosComplement.length - size, data, 0, size);
+ }
+
+ if (le) {
+ BinaryModuleHelper.reverseBytes(data);
+ }
+ return BinaryModuleHelper.createBinaryResult(context, this, data);
+ }
+
+ private Sequence unpackDouble(final Sequence[] args) throws XPathException {
+ final byte[] data = BinaryModuleHelper.getBinaryData(args[0]);
+ final int offset = ((IntegerValue) args[1].itemAt(0)).getInt();
+ final boolean le = getByteOrder(args, 2);
+
+ validateUnpackRange(data, offset, 8);
+
+ final byte[] slice = Arrays.copyOfRange(data, offset, offset + 8);
+ if (le) {
+ BinaryModuleHelper.reverseBytes(slice);
+ }
+ final long bits = ByteBuffer.wrap(slice).getLong();
+ return new DoubleValue(this, Double.longBitsToDouble(bits));
+ }
+
+ private Sequence unpackFloat(final Sequence[] args) throws XPathException {
+ final byte[] data = BinaryModuleHelper.getBinaryData(args[0]);
+ final int offset = ((IntegerValue) args[1].itemAt(0)).getInt();
+ final boolean le = getByteOrder(args, 2);
+
+ validateUnpackRange(data, offset, 4);
+
+ final byte[] slice = Arrays.copyOfRange(data, offset, offset + 4);
+ if (le) {
+ BinaryModuleHelper.reverseBytes(slice);
+ }
+ final int bits = ByteBuffer.wrap(slice).getInt();
+ return new FloatValue(this, Float.intBitsToFloat(bits));
+ }
+
+ private Sequence unpackInteger(final Sequence[] args) throws XPathException {
+ final byte[] data = BinaryModuleHelper.getBinaryData(args[0]);
+ final int offset = ((IntegerValue) args[1].itemAt(0)).getInt();
+ final int size = ((IntegerValue) args[2].itemAt(0)).getInt();
+ final boolean le = getByteOrder(args, 3);
+
+ if (size < 0) {
+ throw new XPathException(this, BinaryModuleErrorCode.NEGATIVE_SIZE,
+ "Size must not be negative: " + size);
+ }
+
+ validateUnpackRange(data, offset, size);
+
+ if (size == 0) {
+ return new IntegerValue(this, 0);
+ }
+
+ final byte[] slice = Arrays.copyOfRange(data, offset, offset + size);
+ if (le) {
+ BinaryModuleHelper.reverseBytes(slice);
+ }
+ // BigInteger(byte[]) interprets as signed two's-complement
+ final BigInteger result = new BigInteger(slice);
+ return new IntegerValue(this, result);
+ }
+
+ private Sequence unpackUnsignedInteger(final Sequence[] args) throws XPathException {
+ final byte[] data = BinaryModuleHelper.getBinaryData(args[0]);
+ final int offset = ((IntegerValue) args[1].itemAt(0)).getInt();
+ final int size = ((IntegerValue) args[2].itemAt(0)).getInt();
+ final boolean le = getByteOrder(args, 3);
+
+ if (size < 0) {
+ throw new XPathException(this, BinaryModuleErrorCode.NEGATIVE_SIZE,
+ "Size must not be negative: " + size);
+ }
+
+ validateUnpackRange(data, offset, size);
+
+ if (size == 0) {
+ return new IntegerValue(this, 0);
+ }
+
+ final byte[] slice = Arrays.copyOfRange(data, offset, offset + size);
+ if (le) {
+ BinaryModuleHelper.reverseBytes(slice);
+ }
+ // BigInteger(1, byte[]) interprets as unsigned (positive signum)
+ final BigInteger result = new BigInteger(1, slice);
+ return new IntegerValue(this, result);
+ }
+
+ private void validateUnpackRange(final byte[] data, final int offset, final int size) throws XPathException {
+ if (data == null) {
+ throw new XPathException(this, BinaryModuleErrorCode.INDEX_OUT_OF_RANGE,
+ "Binary data is empty");
+ }
+ if (offset < 0 || offset + size > data.length) {
+ throw new XPathException(this, BinaryModuleErrorCode.INDEX_OUT_OF_RANGE,
+ "Offset " + offset + " + size " + size + " exceeds binary data length " + data.length);
+ }
+ }
+}
diff --git a/extensions/expath/src/main/java/org/expath/exist/BinaryTextFunctions.java b/extensions/expath/src/main/java/org/expath/exist/BinaryTextFunctions.java
new file mode 100644
index 00000000000..fffae96b826
--- /dev/null
+++ b/extensions/expath/src/main/java/org/expath/exist/BinaryTextFunctions.java
@@ -0,0 +1,194 @@
+/*
+ * eXist-db Open Source Native XML Database
+ * Copyright (C) 2001 The eXist-db Authors
+ *
+ * info@exist-db.org
+ * http://www.exist-db.org
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+package org.expath.exist;
+
+import org.exist.dom.QName;
+import org.exist.xquery.BasicFunction;
+import org.exist.xquery.FunctionSignature;
+import org.exist.xquery.XPathException;
+import org.exist.xquery.XQueryContext;
+import org.exist.xquery.value.IntegerValue;
+import org.exist.xquery.value.Sequence;
+import org.exist.xquery.value.StringValue;
+import org.exist.xquery.value.Type;
+
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CodingErrorAction;
+import java.nio.charset.UnsupportedCharsetException;
+import java.util.Arrays;
+
+import static org.exist.xquery.FunctionDSL.*;
+
+/**
+ * EXPath Binary Module 4.0 — Text Encoding and Decoding (Section 6).
+ *
+ *
+ * - bin:decode-string
+ * - bin:encode-string
+ *
+ *
+ * @see EXPath Binary Module 4.0 §6
+ */
+public class BinaryTextFunctions extends BasicFunction {
+
+ private static final QName QN_DECODE_STRING = new QName("decode-string", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX);
+ private static final QName QN_ENCODE_STRING = new QName("encode-string", BinaryModule.NAMESPACE_URI, BinaryModule.PREFIX);
+
+ static final FunctionSignature[] FS_DECODE_STRING = functionSignatures(
+ QN_DECODE_STRING,
+ "Decodes binary data to an xs:string using the specified encoding.",
+ returnsOpt(Type.STRING),
+ arities(
+ arity(
+ optParam("value", Type.BASE64_BINARY, "The binary data to decode")
+ ),
+ arity(
+ optParam("value", Type.BASE64_BINARY, "The binary data to decode"),
+ param("encoding", Type.STRING, "The character encoding (default: UTF-8)")
+ ),
+ arity(
+ optParam("value", Type.BASE64_BINARY, "The binary data to decode"),
+ param("encoding", Type.STRING, "The character encoding (default: UTF-8)"),
+ param("offset", Type.INTEGER, "The zero-based byte offset to start decoding")
+ ),
+ arity(
+ optParam("value", Type.BASE64_BINARY, "The binary data to decode"),
+ param("encoding", Type.STRING, "The character encoding (default: UTF-8)"),
+ param("offset", Type.INTEGER, "The zero-based byte offset to start decoding"),
+ param("size", Type.INTEGER, "The number of bytes to decode")
+ )
+ )
+ );
+
+ static final FunctionSignature[] FS_ENCODE_STRING = functionSignatures(
+ QN_ENCODE_STRING,
+ "Encodes an xs:string to binary data using the specified encoding.",
+ returnsOpt(Type.BASE64_BINARY),
+ arities(
+ arity(
+ optParam("value", Type.STRING, "The string to encode")
+ ),
+ arity(
+ optParam("value", Type.STRING, "The string to encode"),
+ param("encoding", Type.STRING, "The character encoding (default: UTF-8)")
+ )
+ )
+ );
+
+ public BinaryTextFunctions(final XQueryContext context, final FunctionSignature signature) {
+ super(context, signature);
+ }
+
+ @Override
+ public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException {
+ if (isCalledAs("decode-string")) {
+ return decodeString(args);
+ } else {
+ return encodeString(args);
+ }
+ }
+
+ private Sequence decodeString(final Sequence[] args) throws XPathException {
+ final byte[] data = BinaryModuleHelper.getBinaryData(args[0]);
+ if (data == null) {
+ return Sequence.EMPTY_SEQUENCE;
+ }
+
+ final String encoding = (args.length > 1 && !args[1].isEmpty())
+ ? args[1].getStringValue()
+ : "UTF-8";
+
+ final int offset = (args.length > 2 && !args[2].isEmpty())
+ ? ((IntegerValue) args[2].itemAt(0)).getInt()
+ : 0;
+
+ final int size = (args.length > 3 && !args[3].isEmpty())
+ ? ((IntegerValue) args[3].itemAt(0)).getInt()
+ : data.length - offset;
+
+ if (offset < 0 || offset > data.length) {
+ throw new XPathException(this, BinaryModuleErrorCode.INDEX_OUT_OF_RANGE,
+ "Offset " + offset + " is out of range for binary data of length " + data.length);
+ }
+
+ if (size < 0) {
+ throw new XPathException(this, BinaryModuleErrorCode.NEGATIVE_SIZE,
+ "Size must not be negative: " + size);
+ }
+
+ if (offset + size > data.length) {
+ throw new XPathException(this, BinaryModuleErrorCode.INDEX_OUT_OF_RANGE,
+ "Offset " + offset + " + size " + size + " exceeds binary data length " + data.length);
+ }
+
+ final Charset charset = resolveCharset(encoding);
+
+ try {
+ final CharsetDecoder decoder = charset.newDecoder()
+ .onMalformedInput(CodingErrorAction.REPORT)
+ .onUnmappableCharacter(CodingErrorAction.REPORT);
+ final CharBuffer result = decoder.decode(ByteBuffer.wrap(data, offset, size));
+ return new StringValue(this, result.toString());
+ } catch (final CharacterCodingException e) {
+ throw new XPathException(this, BinaryModuleErrorCode.CONVERSION_ERROR,
+ "Failed to decode binary data using encoding '" + encoding + "': " + e.getMessage());
+ }
+ }
+
+ private Sequence encodeString(final Sequence[] args) throws XPathException {
+ if (args[0].isEmpty()) {
+ return Sequence.EMPTY_SEQUENCE;
+ }
+
+ final String value = args[0].getStringValue();
+ final String encoding = (args.length > 1 && !args[1].isEmpty())
+ ? args[1].getStringValue()
+ : "UTF-8";
+
+ final Charset charset = resolveCharset(encoding);
+
+ try {
+ final ByteBuffer encoded = charset.newEncoder()
+ .onMalformedInput(CodingErrorAction.REPORT)
+ .onUnmappableCharacter(CodingErrorAction.REPORT)
+ .encode(CharBuffer.wrap(value));
+ final byte[] data = Arrays.copyOf(encoded.array(), encoded.limit());
+ return BinaryModuleHelper.createBinaryResult(context, this, data);
+ } catch (final CharacterCodingException e) {
+ throw new XPathException(this, BinaryModuleErrorCode.CONVERSION_ERROR,
+ "Failed to encode string using encoding '" + encoding + "': " + e.getMessage());
+ }
+ }
+
+ private Charset resolveCharset(final String encoding) throws XPathException {
+ try {
+ return Charset.forName(encoding);
+ } catch (final UnsupportedCharsetException e) {
+ throw new XPathException(this, BinaryModuleErrorCode.UNKNOWN_ENCODING,
+ "Unknown encoding: '" + encoding + "'");
+ }
+ }
+}
diff --git a/extensions/expath/src/test/java/org/expath/exist/BinaryModuleTest.java b/extensions/expath/src/test/java/org/expath/exist/BinaryModuleTest.java
new file mode 100644
index 00000000000..f7ab6a2e403
--- /dev/null
+++ b/extensions/expath/src/test/java/org/expath/exist/BinaryModuleTest.java
@@ -0,0 +1,512 @@
+/*
+ * eXist-db Open Source Native XML Database
+ * Copyright (C) 2001 The eXist-db Authors
+ *
+ * info@exist-db.org
+ * http://www.exist-db.org
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+package org.expath.exist;
+
+import org.exist.EXistException;
+import org.exist.security.PermissionDeniedException;
+import org.exist.storage.BrokerPool;
+import org.exist.storage.DBBroker;
+import org.exist.test.ExistEmbeddedServer;
+import org.exist.xquery.XPathException;
+import org.exist.xquery.XQuery;
+import org.exist.xquery.value.Sequence;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Tests for the EXPath Binary Module 4.0 implementation.
+ */
+public class BinaryModuleTest {
+
+ @ClassRule
+ public static ExistEmbeddedServer existEmbeddedServer = new ExistEmbeddedServer(true, true);
+
+ private static final String IMPORT = "import module namespace bin = 'http://expath.org/ns/binary';\n";
+
+ /**
+ * Helper to compare binary results as hex strings.
+ * Wraps the XQuery expression in string(xs:hexBinary(...)) to get a hex string representation.
+ */
+ private void assertBinaryAsHex(final String expectedHex, final String binaryExpr) throws Exception {
+ assertQuery(expectedHex,
+ IMPORT + "string(xs:hexBinary(" + binaryExpr + "))");
+ }
+
+ // ========== Conversion Functions ==========
+
+ @Test
+ public void hex() throws Exception {
+ assertBinaryAsHex("DEADBEEF", "bin:hex('DEADBEEF')");
+ }
+
+ @Test
+ public void hexWithWhitespace() throws Exception {
+ assertBinaryAsHex("DEADBEEF", "bin:hex('DE AD BE EF')");
+ }
+
+ @Test
+ public void hexWithUnderscores() throws Exception {
+ assertBinaryAsHex("DEADBEEF", "bin:hex('DEAD_BEEF')");
+ }
+
+ @Test
+ public void hexOddLength() throws Exception {
+ assertBinaryAsHex("0F", "bin:hex('F')");
+ }
+
+ @Test
+ public void hexEmpty() throws Exception {
+ assertQueryEmpty(IMPORT + "bin:hex(())");
+ }
+
+ @Test
+ public void hexEmptyString() throws Exception {
+ assertQuery("0",
+ IMPORT + "bin:length(bin:hex(''))");
+ }
+
+ @Test
+ public void hexInvalidChar() throws Exception {
+ assertQueryError("non-numeric-character",
+ IMPORT + "bin:hex('GG')");
+ }
+
+ @Test
+ public void bin() throws Exception {
+ assertBinaryAsHex("FF", "bin:bin('11111111')");
+ }
+
+ @Test
+ public void binPadding() throws Exception {
+ assertBinaryAsHex("01", "bin:bin('1')");
+ }
+
+ @Test
+ public void binEmpty() throws Exception {
+ assertQueryEmpty(IMPORT + "bin:bin(())");
+ }
+
+ @Test
+ public void binInvalidChar() throws Exception {
+ assertQueryError("non-numeric-character",
+ IMPORT + "bin:bin('102')");
+ }
+
+ @Test
+ public void octal() throws Exception {
+ assertBinaryAsHex("FF", "bin:octal('377')");
+ }
+
+ @Test
+ public void octalEmpty() throws Exception {
+ assertQueryEmpty(IMPORT + "bin:octal(())");
+ }
+
+ @Test
+ public void octalInvalidChar() throws Exception {
+ assertQueryError("non-numeric-character",
+ IMPORT + "bin:octal('89')");
+ }
+
+ @Test
+ public void toOctets() throws Exception {
+ assertQuery("222 173 190 239",
+ IMPORT + "string-join(for $o in bin:to-octets(bin:hex('DEADBEEF')) return string($o), ' ')");
+ }
+
+ @Test
+ public void fromOctets() throws Exception {
+ assertBinaryAsHex("FF00", "bin:from-octets((255, 0))");
+ }
+
+ @Test
+ public void fromOctetsEmpty() throws Exception {
+ assertQuery("0",
+ IMPORT + "bin:length(bin:from-octets(()))");
+ }
+
+ // ========== Basic Functions ==========
+
+ @Test
+ public void length() throws Exception {
+ assertQuery("4",
+ IMPORT + "bin:length(bin:hex('DEADBEEF'))");
+ }
+
+ @Test
+ public void part() throws Exception {
+ assertBinaryAsHex("ADBE", "bin:part(bin:hex('DEADBEEF'), 1, 2)");
+ }
+
+ @Test
+ public void partNoSize() throws Exception {
+ assertBinaryAsHex("BEEF", "bin:part(bin:hex('DEADBEEF'), 2)");
+ }
+
+ @Test
+ public void partEmpty() throws Exception {
+ assertQueryEmpty(IMPORT + "bin:part((), 0, 1)");
+ }
+
+ @Test
+ public void partOutOfRange() throws Exception {
+ assertQueryError("index-out-of-range",
+ IMPORT + "bin:part(bin:hex('FF'), 0, 5)");
+ }
+
+ @Test
+ public void partNegativeSize() throws Exception {
+ assertQueryError("negative-size",
+ IMPORT + "bin:part(bin:hex('FF'), 0, -1)");
+ }
+
+ @Test
+ public void join() throws Exception {
+ assertBinaryAsHex("DEADBEEF", "bin:join((bin:hex('DEAD'), bin:hex('BEEF')))");
+ }
+
+ @Test
+ public void joinEmpty() throws Exception {
+ assertQuery("0",
+ IMPORT + "bin:length(bin:join(()))");
+ }
+
+ @Test
+ public void insertBefore() throws Exception {
+ assertBinaryAsHex("DEFFADBE", "bin:insert-before(bin:hex('DEADBE'), 1, bin:hex('FF'))");
+ }
+
+ @Test
+ public void insertBeforeEmpty() throws Exception {
+ assertQueryEmpty(IMPORT + "bin:insert-before((), 0, bin:hex('FF'))");
+ }
+
+ @Test
+ public void insertBeforeOutOfRange() throws Exception {
+ assertQueryError("index-out-of-range",
+ IMPORT + "bin:insert-before(bin:hex('FF'), 5, bin:hex('00'))");
+ }
+
+ @Test
+ public void padLeft() throws Exception {
+ assertBinaryAsHex("0000FF", "bin:pad-left(bin:hex('FF'), 2)");
+ }
+
+ @Test
+ public void padLeftWithOctet() throws Exception {
+ assertBinaryAsHex("ABABFF", "bin:pad-left(bin:hex('FF'), 2, 171)");
+ }
+
+ @Test
+ public void padLeftNegativeCount() throws Exception {
+ assertQueryError("negative-size",
+ IMPORT + "bin:pad-left(bin:hex('FF'), -1)");
+ }
+
+ @Test
+ public void padRight() throws Exception {
+ assertBinaryAsHex("FF0000", "bin:pad-right(bin:hex('FF'), 2)");
+ }
+
+ @Test
+ public void padRightWithOctet() throws Exception {
+ assertBinaryAsHex("FFABAB", "bin:pad-right(bin:hex('FF'), 2, 171)");
+ }
+
+ @Test
+ public void find() throws Exception {
+ assertQuery("1",
+ IMPORT + "bin:find(bin:hex('DEADBEEF'), 0, bin:hex('ADBE'))");
+ }
+
+ @Test
+ public void findNotFound() throws Exception {
+ assertQueryEmpty(IMPORT + "bin:find(bin:hex('DEADBEEF'), 0, bin:hex('CAFE'))");
+ }
+
+ @Test
+ public void findFromOffset() throws Exception {
+ assertQueryEmpty(IMPORT + "bin:find(bin:hex('DEADBEEF'), 2, bin:hex('DEAD'))");
+ }
+
+ @Test
+ public void findEmpty() throws Exception {
+ assertQueryEmpty(IMPORT + "bin:find((), 0, bin:hex('FF'))");
+ }
+
+ // ========== Text Functions ==========
+
+ @Test
+ public void decodeStringUtf8() throws Exception {
+ assertQuery("hello",
+ IMPORT + "bin:decode-string(bin:encode-string('hello'))");
+ }
+
+ @Test
+ public void decodeStringWithEncoding() throws Exception {
+ assertQuery("hello",
+ IMPORT + "bin:decode-string(bin:encode-string('hello', 'UTF-8'), 'UTF-8')");
+ }
+
+ @Test
+ public void decodeStringWithOffset() throws Exception {
+ assertQuery("llo",
+ IMPORT + "bin:decode-string(bin:encode-string('hello', 'UTF-8'), 'UTF-8', 2)");
+ }
+
+ @Test
+ public void decodeStringWithOffsetAndSize() throws Exception {
+ assertQuery("ll",
+ IMPORT + "bin:decode-string(bin:encode-string('hello', 'UTF-8'), 'UTF-8', 2, 2)");
+ }
+
+ @Test
+ public void decodeStringEmpty() throws Exception {
+ assertQueryEmpty(IMPORT + "bin:decode-string(())");
+ }
+
+ @Test
+ public void encodeStringUtf8() throws Exception {
+ assertBinaryAsHex("41", "bin:encode-string('A')");
+ }
+
+ @Test
+ public void encodeStringEmpty() throws Exception {
+ assertQueryEmpty(IMPORT + "bin:encode-string(())");
+ }
+
+ @Test
+ public void encodeStringUnknownEncoding() throws Exception {
+ assertQueryError("unknown-encoding",
+ IMPORT + "bin:encode-string('hello', 'NONEXISTENT')");
+ }
+
+ @Test
+ public void decodeStringOutOfRange() throws Exception {
+ assertQueryError("index-out-of-range",
+ IMPORT + "bin:decode-string(bin:encode-string('hi'), 'UTF-8', 100)");
+ }
+
+ // ========== Packing Functions ==========
+
+ @Test
+ public void packUnpackDouble() throws Exception {
+ assertQuery("true",
+ IMPORT + "bin:unpack-double(bin:pack-double(3.14), 0) = 3.14");
+ }
+
+ @Test
+ public void packUnpackDoubleLittleEndian() throws Exception {
+ assertQuery("true",
+ IMPORT + "bin:unpack-double(bin:pack-double(3.14, 'LE'), 0, 'LE') = 3.14");
+ }
+
+ @Test
+ public void packDoubleSize() throws Exception {
+ assertQuery("8",
+ IMPORT + "bin:length(bin:pack-double(1.0))");
+ }
+
+ @Test
+ public void packUnpackFloat() throws Exception {
+ assertQuery("true",
+ IMPORT + "abs(bin:unpack-float(bin:pack-float(xs:float(3.14)), 0) - 3.14) < 0.001");
+ }
+
+ @Test
+ public void packFloatSize() throws Exception {
+ assertQuery("4",
+ IMPORT + "bin:length(bin:pack-float(xs:float(1.0)))");
+ }
+
+ @Test
+ public void packUnpackInteger() throws Exception {
+ assertQuery("42",
+ IMPORT + "bin:unpack-integer(bin:pack-integer(42, 4), 0, 4)");
+ }
+
+ @Test
+ public void packUnpackNegativeInteger() throws Exception {
+ assertQuery("-1",
+ IMPORT + "bin:unpack-integer(bin:pack-integer(-1, 4), 0, 4)");
+ }
+
+ @Test
+ public void packUnpackIntegerLittleEndian() throws Exception {
+ assertQuery("256",
+ IMPORT + "bin:unpack-integer(bin:pack-integer(256, 4, 'LE'), 0, 4, 'LE')");
+ }
+
+ @Test
+ public void packIntegerNegativeSize() throws Exception {
+ assertQueryError("negative-size",
+ IMPORT + "bin:pack-integer(42, -1)");
+ }
+
+ @Test
+ public void unpackUnsignedInteger() throws Exception {
+ assertQuery("255",
+ IMPORT + "bin:unpack-unsigned-integer(bin:hex('FF'), 0, 1)");
+ }
+
+ @Test
+ public void unpackUnsignedIntegerVsSigned() throws Exception {
+ assertQuery("-1",
+ IMPORT + "bin:unpack-integer(bin:hex('FF'), 0, 1)");
+ }
+
+ @Test
+ public void unpackDoubleOutOfRange() throws Exception {
+ assertQueryError("index-out-of-range",
+ IMPORT + "bin:unpack-double(bin:hex('FF'), 0)");
+ }
+
+ @Test
+ public void packUnpackDoubleNaN() throws Exception {
+ assertQuery("true",
+ IMPORT + "let $nan := xs:double('NaN') return "
+ + "bin:unpack-double(bin:pack-double($nan), 0) != bin:unpack-double(bin:pack-double($nan), 0)");
+ }
+
+ @Test
+ public void invalidOctetOrder() throws Exception {
+ assertQueryError("XPTY0004",
+ IMPORT + "bin:pack-double(1.0, 'INVALID')");
+ }
+
+ // ========== Bitwise Functions ==========
+
+ @Test
+ public void bitwiseOr() throws Exception {
+ assertBinaryAsHex("FF", "bin:or(bin:hex('F0'), bin:hex('0F'))");
+ }
+
+ @Test
+ public void bitwiseXor() throws Exception {
+ assertBinaryAsHex("FF", "bin:xor(bin:hex('F0'), bin:hex('0F'))");
+ }
+
+ @Test
+ public void bitwiseAnd() throws Exception {
+ assertBinaryAsHex("F0", "bin:and(bin:hex('FF'), bin:hex('F0'))");
+ }
+
+ @Test
+ public void bitwiseNot() throws Exception {
+ assertBinaryAsHex("0F", "bin:not(bin:hex('F0'))");
+ }
+
+ @Test
+ public void bitwiseOrEmpty() throws Exception {
+ assertQueryEmpty(IMPORT + "bin:or((), bin:hex('FF'))");
+ }
+
+ @Test
+ public void bitwiseDifferingLength() throws Exception {
+ assertQueryError("differing-length-arguments",
+ IMPORT + "bin:or(bin:hex('FF'), bin:hex('FFFF'))");
+ }
+
+ @Test
+ public void shiftLeft() throws Exception {
+ assertBinaryAsHex("10", "bin:shift(bin:hex('01'), 4)");
+ }
+
+ @Test
+ public void shiftRight() throws Exception {
+ assertBinaryAsHex("08", "bin:shift(bin:hex('80'), -4)");
+ }
+
+ @Test
+ public void shiftLeftOverflow() throws Exception {
+ assertBinaryAsHex("00", "bin:shift(bin:hex('FF'), 8)");
+ }
+
+ @Test
+ public void shiftEmpty() throws Exception {
+ assertQueryEmpty(IMPORT + "bin:shift((), 1)");
+ }
+
+ // ========== Roundtrip Tests ==========
+
+ @Test
+ public void hexRoundtrip() throws Exception {
+ assertQuery("222 173 190 239",
+ IMPORT + "string-join("
+ + "for $o in bin:to-octets(bin:hex('DEADBEEF')) return string($o), ' ')");
+ }
+
+ @Test
+ public void octetsRoundtrip() throws Exception {
+ assertBinaryAsHex("DEADBEEF", "bin:from-octets(bin:to-octets(bin:hex('DEADBEEF')))");
+ }
+
+ @Test
+ public void textRoundtrip() throws Exception {
+ assertQuery("Hello, World!",
+ IMPORT + "bin:decode-string(bin:encode-string('Hello, World!'))");
+ }
+
+ @Test
+ public void textRoundtripUtf16() throws Exception {
+ assertQuery("Hello",
+ IMPORT + "bin:decode-string(bin:encode-string('Hello', 'UTF-16'), 'UTF-16')");
+ }
+
+ // ========== Helper Methods ==========
+
+ private void assertQuery(final String expected, final String query) throws Exception {
+ final Sequence result = executeQuery(query);
+ assertEquals("Query: " + query, expected, result.getStringValue());
+ }
+
+ private void assertQueryEmpty(final String query) throws Exception {
+ final Sequence result = executeQuery(query);
+ assertTrue("Expected empty sequence for query: " + query, result.isEmpty());
+ }
+
+ private void assertQueryError(final String errorCode, final String query) {
+ try {
+ executeQuery(query);
+ fail("Expected error " + errorCode + " for query: " + query);
+ } catch (final XPathException e) {
+ final String msg = e.getMessage();
+ final String code = e.getErrorCode() != null ? e.getErrorCode().getErrorQName().getLocalPart() : "";
+ assertTrue("Expected error containing '" + errorCode + "' but got: " + code + " - " + msg,
+ code.contains(errorCode) || msg.contains(errorCode));
+ } catch (final Exception e) {
+ fail("Expected XPathException with " + errorCode + " but got: " + e.getClass().getSimpleName() + " - " + e.getMessage());
+ }
+ }
+
+ private Sequence executeQuery(final String query) throws EXistException, PermissionDeniedException, XPathException {
+ final BrokerPool brokerPool = existEmbeddedServer.getBrokerPool();
+ final XQuery xquery = brokerPool.getXQueryService();
+ try (final DBBroker broker = brokerPool.getBroker()) {
+ return xquery.execute(broker, query, null);
+ }
+ }
+}
diff --git a/extensions/expath/src/test/resources-filtered/conf.xml b/extensions/expath/src/test/resources-filtered/conf.xml
index a0e02a2c06d..a262a155143 100644
--- a/extensions/expath/src/test/resources-filtered/conf.xml
+++ b/extensions/expath/src/test/resources-filtered/conf.xml
@@ -745,6 +745,7 @@
+