Skip to content

Commit

Permalink
Merge pull request #708 from lognaturel/base64
Browse files Browse the repository at this point in the history
Add base64-decode
  • Loading branch information
seadowg authored Mar 16, 2023
2 parents 83c9f2b + 8f73505 commit 96c37ee
Show file tree
Hide file tree
Showing 10 changed files with 307 additions and 75 deletions.
185 changes: 121 additions & 64 deletions src/main/java/org/javarosa/xpath/expr/Encoding.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,82 +15,139 @@
*/
package org.javarosa.xpath.expr;

import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import org.javarosa.xpath.XPathUnsupportedException;

/**
* Implements the hash encoding methods for XPathFuncExpr digest() function
*/
enum Encoding {
HEX("hex") {
@Override
String encode(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
sb.append(HEX_TBL[(b >> 4) & 0xF]);
sb.append(HEX_TBL[(b & 0xF)]);
}
return sb.toString();
HEX("hex") {
@Override
String encode(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
sb.append(HEX_TBL[(b >> 4) & 0xF]);
sb.append(HEX_TBL[(b & 0xF)]);
}
return sb.toString();
}

@Override
byte[] decode(byte[] bytes) {
int len = bytes.length;
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(bytes[i], 16) << 4)
+ Character.digit(bytes[i+1], 16));
}
return data;
}
},
BASE64("base64") {
// Copied from: https://github.com/brsanthu/migbase64/blob/master/src/main/java/com/migcomponents/migbase64/Base64.java
// Irrelevant after Java8
@Override
String encode(byte[] sArr) {
int sLen = sArr.length;
int sOff = 0;

if (sLen == 0)
return "";

int eLen = (sLen / 3) * 3;
int dLen = ((sLen - 1) / 3 + 1) << 2;
byte[] dArr = new byte[dLen];

for (int s = sOff, d = 0; s < sOff + eLen; ) {
int i = (sArr[s++] & 0xff) << 16 | (sArr[s++] & 0xff) << 8 | (sArr[s++] & 0xff);
dArr[d++] = (byte) BASE_64_TBL[(i >>> 18) & 0x3f];
dArr[d++] = (byte) BASE_64_TBL[(i >>> 12) & 0x3f];
dArr[d++] = (byte) BASE_64_TBL[(i >>> 6) & 0x3f];
dArr[d++] = (byte) BASE_64_TBL[i & 0x3f];
}

int left = sLen - eLen;
if (left > 0) {
int i = ((sArr[sOff + eLen] & 0xff) << 10) | (left == 2 ? ((sArr[sOff + sLen - 1] & 0xff) << 2) : 0);
dArr[dLen - 4] = (byte) BASE_64_TBL[i >> 12];
dArr[dLen - 3] = (byte) BASE_64_TBL[(i >>> 6) & 0x3f];
dArr[dLen - 2] = left == 2 ? (byte) BASE_64_TBL[i & 0x3f] : (byte) '=';
dArr[dLen - 1] = '=';
}

return new String(dArr, StandardCharsets.UTF_8);
}

@Override
public byte[] decode(byte[] sArr) {
int sepCnt = 0;
for (byte b : sArr)
if (IA[b & 0xff] < 0)
sepCnt++;

if ((sArr.length - sepCnt) % 4 != 0)
return new byte[0];

int pad = 0;
for (int i = sArr.length; i > 1 && IA[sArr[--i] & 0xff] <= 0; )
if (sArr[i] == '=')
pad++;

int len = ((sArr.length - sepCnt) * 6 >> 3) - pad;

byte[] dArr = new byte[len];

for (int s = 0, d = 0; d < len; ) {
int i = 0;
for (int j = 0; j < 4; j++) {
int c = IA[sArr[s++] & 0xff];
if (c >= 0)
i |= c << (18 - j * 6);
else
j--;
}

dArr[d++] = (byte) (i >> 16);
if (d < len) {
dArr[d++] = (byte) (i >> 8);
if (d < len)
dArr[d++] = (byte) i;
}
}

return dArr;
}
};

private final String name;

Encoding(String name) {
this.name = name;
}
},
BASE64("base64") {
@Override
String encode(byte[] sArr) {
// Copied from: https://github.com/brsanthu/migbase64/blob/master/src/main/java/com/migcomponents/migbase64/Base64.java
// Irrelevant after Java8
int sLen = sArr.length;
int sOff = 0;

if (sLen == 0)
return "";

int eLen = (sLen / 3) * 3;
int dLen = ((sLen - 1) / 3 + 1) << 2;
byte[] dArr = new byte[dLen];

for (int s = sOff, d = 0; s < sOff + eLen; ) {
int i = (sArr[s++] & 0xff) << 16 | (sArr[s++] & 0xff) << 8 | (sArr[s++] & 0xff);
dArr[d++] = (byte) BASE_64_TBL[(i >>> 18) & 0x3f];
dArr[d++] = (byte) BASE_64_TBL[(i >>> 12) & 0x3f];
dArr[d++] = (byte) BASE_64_TBL[(i >>> 6) & 0x3f];
dArr[d++] = (byte) BASE_64_TBL[i & 0x3f];
}

int left = sLen - eLen;
if (left > 0) {
int i = ((sArr[sOff + eLen] & 0xff) << 10) | (left == 2 ? ((sArr[sOff + sLen - 1] & 0xff) << 2) : 0);
dArr[dLen - 4] = (byte) BASE_64_TBL[i >> 12];
dArr[dLen - 3] = (byte) BASE_64_TBL[(i >>> 6) & 0x3f];
dArr[dLen - 2] = left == 2 ? (byte) BASE_64_TBL[i & 0x3f] : (byte) '=';
dArr[dLen - 1] = '=';
}

try {
return new String(dArr, "UTF-8");
} catch (UnsupportedEncodingException e) {
// It’s unlikely that UTF-8 would not be supported
throw new RuntimeException("Encoding to base64 failed to use UTF-8");
}

static Encoding from(String name) {
try {
return valueOf(name.toUpperCase());
} catch (IllegalArgumentException ex) {
throw new XPathUnsupportedException("digest(..., ..., '" + name + "')");
}
}
};

private final String name;
private static final char[] HEX_TBL = "0123456789abcdef".toCharArray();

Encoding(String name) {
this.name = name;
}
private static final char[] BASE_64_TBL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray();

static Encoding from(String name) {
try {
return valueOf(name.toUpperCase());
} catch (IllegalArgumentException ex) {
throw new XPathUnsupportedException("digest(..., ..., '" + name + "')");
private static final int[] IA = new int[256];
static {
Arrays.fill(IA, -1);
for (int i = 0, iS = BASE_64_TBL.length; i < iS; i++)
IA[BASE_64_TBL[i]] = i;
IA['='] = 0;
}
}

private static final char[] HEX_TBL = "0123456789abcdef".toCharArray();

private static final char[] BASE_64_TBL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray();
abstract String encode(byte[] bytes);

abstract String encode(byte[] bytes);
abstract byte[] decode(byte[] bytes);
}
15 changes: 13 additions & 2 deletions src/main/java/org/javarosa/xpath/expr/XPathFuncExpr.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.io.DataOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
Expand Down Expand Up @@ -486,6 +487,9 @@ public Object eval(DataInstance model, EvaluationContext evalContext) {
return XPathNodeset.shuffle((XPathNodeset) argVals[0], toNumeric(argVals[1]).longValue());

throw new XPathUnhandledException("function \'randomize\' requires 1 or 2 arguments. " + args.length + " provided.");
} else if (name.equals("base64-decode")) {
assertArgsCount(name, args, 1);
return base64Decode(argVals[0]);
} else {
//check for custom handler
IFunctionHandler handler = funcHandlers.get(name);
Expand All @@ -503,8 +507,8 @@ public Object eval(DataInstance model, EvaluationContext evalContext) {

private static void assertArgsCount(String name, Object[] args, int count) {
if (args.length != count) {
throw new XPathUnhandledException("function \'" + name + "\' requires " +
count + " arguments. Only " + args.length + " provided.");
throw new XPathUnhandledException("function '" + name + "'. Requires " +
count + " arguments but " + args.length + " provided.");
}
}

Expand Down Expand Up @@ -1247,6 +1251,13 @@ public static Boolean regex(Object o1, Object o2) {
return Pattern.matches(re, str);
}

private static String base64Decode(Object o1) {
String base64String = toString(o1);

byte[] decoded = Encoding.BASE64.decode(base64String.getBytes(StandardCharsets.UTF_8));
return new String(decoded, StandardCharsets.UTF_8);
}

private static Object[] subsetArgList(Object[] args, int start) {
return subsetArgList(args, start, 1);
}
Expand Down
123 changes: 123 additions & 0 deletions src/test/java/org/javarosa/xpath/expr/Base64DecodeTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package org.javarosa.xpath.expr;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.javarosa.core.test.AnswerDataMatchers.stringAnswer;
import static org.javarosa.core.util.BindBuilderXFormsElement.bind;
import static org.javarosa.core.util.XFormsElement.body;
import static org.javarosa.core.util.XFormsElement.head;
import static org.javarosa.core.util.XFormsElement.html;
import static org.javarosa.core.util.XFormsElement.input;
import static org.javarosa.core.util.XFormsElement.mainInstance;
import static org.javarosa.core.util.XFormsElement.model;
import static org.javarosa.core.util.XFormsElement.t;
import static org.javarosa.core.util.XFormsElement.title;
import static org.junit.Assert.fail;

import java.io.IOException;
import org.javarosa.core.test.Scenario;
import org.javarosa.xform.parse.XFormParser;
import org.javarosa.xpath.XPathUnhandledException;
import org.junit.Test;

public class Base64DecodeTest {

@Test
public void asciiString_isSuccessfullyDecoded() throws IOException, XFormParser.ParseException {
Scenario scenario = getBase64DecodeScenario("ASCII string", "SGVsbG8=");
assertThat(scenario.answerOf("/data/decoded"), is(stringAnswer("Hello")));
}

@Test
public void exampleFromSaxonica_isSuccessfullyDecoded() throws IOException, XFormParser.ParseException {
Scenario scenario = getBase64DecodeScenario("Example from Saxonica", "RGFzc2Vs");
assertThat(scenario.answerOf("/data/decoded"), is(stringAnswer("Dassel")));
}

@Test
public void accentString_isSuccessfullyDecoded() throws IOException, XFormParser.ParseException {
Scenario scenario = getBase64DecodeScenario("String with accented characters", "w6nDqMOx");
assertThat(scenario.answerOf("/data/decoded"), is(stringAnswer("éèñ")));
}

@Test
public void emojiString_isSuccessfullyDecoded() throws IOException, XFormParser.ParseException {
Scenario scenario = getBase64DecodeScenario("String with emoji", "8J+lsA==");
assertThat(scenario.answerOf("/data/decoded"), is(stringAnswer("🥰")));
}

@Test
public void utf16String_isDecodedToGarbage() throws IOException, XFormParser.ParseException {
Scenario scenario = getBase64DecodeScenario("UTF-16 encoded string", "AGEAYgBj");
assertThat(scenario.answerOf("/data/decoded"), is(stringAnswer("\u0000a\u0000b\u0000c"))); // source string: "abc" in UTF-16
}

private static Scenario getBase64DecodeScenario(String testName, String source) throws IOException, XFormParser.ParseException {
return Scenario.init(testName, html(
head(
title(testName),
model(
mainInstance(t("data id=\"base64\"",
t("text", source),
t("decoded")
)),
bind("/data/text").type("string"),
bind("/data/decoded").type("string").calculate("base64-decode(/data/text)")
)
),
body(
input("/data/text")
))
);
}

@Test
public void base64DecodeFunction_throwsWhenNotExactlyOneArg() throws IOException, XFormParser.ParseException {
try {
Scenario scenario = Scenario.init("Invalid base64 string", html(
head(
title("Invalid base64 string"),
model(
mainInstance(t("data id=\"base64\"",
t("text", "a"),
t("decoded")
)),
bind("/data/text").type("string"),
bind("/data/decoded").type("string").calculate("base64-decode()")
)
),
body(
input("/data/text")
))
);

fail("RuntimeException caused by XPathUnhandledException expected");
} catch (RuntimeException e) {
assertThat(e.getCause(), instanceOf(XPathUnhandledException.class));
}
}

@Test
public void base64DecodeFunction_returnsEmptyStringWhenInputInvalid() throws IOException, XFormParser.ParseException {
Scenario scenario = Scenario.init("Invalid base64 string", html(
head(
title("Invalid base64 string"),
model(
mainInstance(t("data id=\"base64\"",
t("text", "a"),
t("decoded")
)),
bind("/data/text").type("string"),
bind("/data/decoded").type("string").calculate("base64-decode(/data/text)")
)
),
body(
input("/data/text")
))
);

assertThat(scenario.answerOf("/data/decoded"), is(nullValue()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import org.junit.Before;
import org.junit.Test;

public class XPathPathExprCurrentFieldRefTest {
public class CurrentFieldRefTest {
private Scenario scenario;

@Before
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import org.junit.Before;
import org.junit.Test;

public class XPathPathExprCurrentGroupCountRefTest {
public class CurrentGroupCountRefTest {
private Scenario scenario;

@Before
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import org.junit.Before;
import org.junit.Test;

public class XPathPathExprCurrentTest {
public class CurrentTest {
private Scenario scenario;

@Before
Expand Down
Loading

0 comments on commit 96c37ee

Please sign in to comment.