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 @@ +