Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,32 +24,35 @@ import org.codehaus.groovy.runtime.NullObject

import org.apache.commons.codec.binary.Base64

import groovy.transform.CompileStatic

/**
* A codec that encodes and decodes Objects using Base64 encoding.
*
* @author Drew Varner
*/
@CompileStatic
class Base64CodecExtensionMethods {

static encodeAsBase64(theTarget) {
static Object encodeAsBase64(Object theTarget) {
if (theTarget == null || theTarget instanceof NullObject) {
return null
}

if (theTarget instanceof Byte[] || theTarget instanceof byte[]) {
return new String(Base64.encodeBase64(theTarget))
return new String(Base64.encodeBase64(DigestUtils.toByteArray(theTarget)), StandardCharsets.UTF_8)
Comment thread
jdaugherty marked this conversation as resolved.
}

return new String(Base64.encodeBase64(theTarget.toString().getBytes(StandardCharsets.UTF_8)))
return new String(Base64.encodeBase64(theTarget.toString().getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8)
}

static decodeBase64(theTarget) {
static Object decodeBase64(Object theTarget) {
if (theTarget == null || theTarget instanceof NullObject) {
return null
}

if (theTarget instanceof Byte[] || theTarget instanceof byte[]) {
return Base64.decodeBase64(theTarget)
return Base64.decodeBase64(DigestUtils.toByteArray(theTarget))
}

return Base64.decodeBase64(theTarget.toString().getBytes(StandardCharsets.UTF_8))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should take the opportunity to make UTF_8 an added method argument that's defaulted

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,69 @@
*/
package org.grails.plugins.codecs

import java.lang.reflect.Array
import java.nio.charset.StandardCharsets
import java.security.MessageDigest

import groovy.transform.CompileStatic

@CompileStatic
abstract class DigestUtils {

// Digest byte[], any list/array or string into a byte[]
static digest(String algorithm, data) {
static Object digest(String algorithm, Object data) {
if (data == null) {
return null
}

def md = MessageDigest.getInstance(algorithm)
def src
if (data instanceof Byte[] || data instanceof byte[]) {
src = data
MessageDigest md = MessageDigest.getInstance(algorithm)
byte[] src = toByteArray(data)
md.update(src) // This probably needs to use the thread's Locale encoding
return md.digest()
}

protected static byte[] toByteArray(Object data) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: protected reads as "for subclasses", but the actual consumers (Base64CodecExtensionMethods, HexCodecExtensionMethods) rely on protected's package-access side effect. @PackageScope (or a brief comment) would make the intent clearer.

if (data instanceof byte[]) {
return (byte[]) data
}
else if (data instanceof List || data.getClass().isArray()) {
src = new byte[data.size()]
data.eachWithIndex { v, i -> src[i] = v }
if (data instanceof Byte[]) {
return toByteArrayFromWrapper((Byte[]) data)
}
else {
src = data.toString().getBytes(StandardCharsets.UTF_8)
if (data instanceof List) {
return toByteArrayFromList((List<?>) data)
}
md.update(src) // This probably needs to use the thread's Locale encoding
return md.digest()
if (data.getClass().isArray()) {
return toByteArrayFromArray(data, Array.getLength(data))
}

return data.toString().getBytes(StandardCharsets.UTF_8)
}

private static byte[] toByteArrayFromWrapper(Byte[] data) {
byte[] result = new byte[data.length]
for (int i = 0; i < data.length; i++) {
result[i] = data[i].byteValue()
}
return result
}

private static byte[] toByteArrayFromList(List<?> data) {
byte[] result = new byte[data.size()]
for (int i = 0; i < data.size(); i++) {
result[i] = toByte(data.get(i))
}
return result
}

private static byte[] toByteArrayFromArray(Object data, int length) {
byte[] result = new byte[length]
for (int i = 0; i < length; i++) {
result[i] = toByte(Array.get(data, i))
}
return result
}

private static byte toByte(Object value) {
return ((Number) value).byteValue()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This narrows the accepted element types compared to the old dynamic code. Previously src[i] = v went through Groovy's DefaultTypeTransformation coercion, which also accepted Character elements (and char[] arrays via the array path); now any non-Number element throws ClassCastException. If preserving the old tolerance matters, DefaultTypeTransformation.castToNumber(value).byteValue() keeps the coercion while staying statically compiled. If the narrowing is intentional, it's probably worth a line in the PR compatibility notes.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,50 +22,45 @@ import java.nio.charset.StandardCharsets

import org.codehaus.groovy.runtime.NullObject

import groovy.transform.CompileStatic

@CompileStatic
class HexCodecExtensionMethods {

static HEXDIGITS = '0123456789abcdef'
static Object HEXDIGITS = '0123456789abcdef'

// Expects an array/list of numbers
static encodeAsHex(theTarget) {
static Object encodeAsHex(Object theTarget) {
if (theTarget == null || theTarget instanceof NullObject) {
return null
}

def result = new StringBuilder()
if (theTarget instanceof String) {
theTarget = theTarget.getBytes(StandardCharsets.UTF_8)
}
theTarget.each() {
result << HexCodecExtensionMethods.HEXDIGITS[(it & 0xF0) >> 4]
result << HexCodecExtensionMethods.HEXDIGITS[it & 0x0F]
byte[] bytes = theTarget instanceof String ? ((String) theTarget).getBytes(StandardCharsets.UTF_8) : DigestUtils.toByteArray(theTarget)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Behavior change for iterable targets that aren't Lists or arrays (e.g. a Set<Integer> or an Iterator): the old code element-wise hex-encoded anything via theTarget.each { ... }, but DigestUtils.toByteArray only special-cases byte[]/Byte[]/List/arrays and otherwise falls through to toString().getBytes(UTF_8) — so a Set of numbers now silently encodes its string representation instead of its elements. Consider handling Collection/Iterable in toByteArray (or at least calling this out in the compatibility notes, since the failure mode is silent wrong output rather than an exception).

StringBuilder result = new StringBuilder(bytes.length * 2)
String hexDigits = (String) HEXDIGITS
for (byte value : bytes) {
int unsignedValue = value & 0xFF
result.append(hexDigits.charAt((unsignedValue & 0xF0) >> 4))
result.append(hexDigits.charAt(unsignedValue & 0x0F))
}
return result.toString()
}

static decodeHex(theTarget) {
if (!theTarget) return null
static Object decodeHex(Object theTarget) {
if (theTarget == null || theTarget instanceof NullObject || theTarget.toString().length() == 0) return null

Comment thread
jamesfredley marked this conversation as resolved.
def output = []

def str = theTarget.toString().toLowerCase()
if (str.size() % 2) {
String str = theTarget.toString().toLowerCase()
if (str.size() % 2 != 0) {
throw new UnsupportedOperationException('Decode of hex strings requires strings of even length')
}

def currentByte
str.eachWithIndex { val, idx ->
if (!(idx % 2)) {
currentByte = HEXDIGITS.indexOf(val) << 4
}
else {
output << (currentByte | HEXDIGITS.indexOf(val))
currentByte = 0
}
byte[] result = new byte[str.size().intdiv(2)]
String hexDigits = (String) HEXDIGITS
for (int i = 0; i < str.size(); i += 2) {
int high = hexDigits.indexOf((int) str.charAt(i))
int low = hexDigits.indexOf((int) str.charAt(i + 1))
result[i.intdiv(2)] = (byte) ((high << 4) | low)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit for a perf-focused PR: i.intdiv(2) boxes on every iteration; i >> 1 (and str.length() >> 1 on line 58) avoids it. Same for str.size() vs plain str.length(). Also, on line 51 the explicit null/NullObject checks are redundant — DefaultTypeTransformation.castToBoolean already returns false for both.

}

def result = new byte[output.size()]
output.eachWithIndex { v, i -> result[i] = v }
return result
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,21 @@ package org.grails.plugins.codecs

import org.codehaus.groovy.runtime.NullObject

import groovy.transform.CompileStatic

@CompileStatic
class MD5BytesCodecExtensionMethods {

// Returns the byte[] of the digest, taken from UTF-8 of the string representation
// or the raw data coerced to bytes
static encodeAsMD5Bytes(theTarget) {
static Object encodeAsMD5Bytes(Object theTarget) {
if (theTarget == null || theTarget instanceof NullObject) {
return null
}
DigestUtils.digest('MD5', theTarget)
}

static decodeMD5Bytes(theTarget) {
static Object decodeMD5Bytes(Object theTarget) {
throw new UnsupportedOperationException('Cannot decode MD5 hashes')
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,19 @@
*/
package org.grails.plugins.codecs

import groovy.transform.CompileStatic

@CompileStatic
class MD5CodecExtensionMethods {

// Returns the byte[] of the digest, taken from UTF-8 of the string representation
// or the raw data coerced to bytes
static encodeAsMD5(theTarget) {
theTarget.encodeAsMD5Bytes()?.encodeAsHex()
static Object encodeAsMD5(Object theTarget) {
byte[] digest = (byte[]) MD5BytesCodecExtensionMethods.encodeAsMD5Bytes(theTarget)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just confirming this is intended: the old theTarget.encodeAsMD5Bytes()?.encodeAsHex() dispatched dynamically, so a runtime metaclass override of encodeAsMD5Bytes (e.g. an application-supplied codec replacing the default) would also change what encodeAsMD5 produced. The direct static call hardwires the default implementation. Same applies to the SHA-1/SHA-256 variants. Probably fine — it's the whole point of removing dynamic dispatch — but worth a note in the compatibility section.

return digest == null ? null : HexCodecExtensionMethods.encodeAsHex(digest)
}

static decodeMD5(theTarget) {
static Object decodeMD5(Object theTarget) {
throw new UnsupportedOperationException('Cannot decode MD5 hashes')
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,20 @@ package org.grails.plugins.codecs

import org.codehaus.groovy.runtime.NullObject

import groovy.transform.CompileStatic

@CompileStatic
class SHA1BytesCodecExtensionMethods {

// Returns the byte[] of the digest
static encodeAsSHA1Bytes(theTarget) {
static Object encodeAsSHA1Bytes(Object theTarget) {
if (theTarget == null || theTarget instanceof NullObject) {
return null
}
DigestUtils.digest('SHA-1', theTarget)
}

static decodeSHA1Bytes(theTarget) {
static Object decodeSHA1Bytes(Object theTarget) {
throw new UnsupportedOperationException('Cannot decode SHA-1 hashes')
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,20 @@ package org.grails.plugins.codecs

import org.codehaus.groovy.runtime.NullObject

import groovy.transform.CompileStatic

@CompileStatic
class SHA1CodecExtensionMethods {

// Returns the byte[] of the digest
static encodeAsSHA1(theTarget) {
static Object encodeAsSHA1(Object theTarget) {
if (theTarget == null || theTarget instanceof NullObject) {
return null
}
theTarget.encodeAsSHA1Bytes().encodeAsHex()
return HexCodecExtensionMethods.encodeAsHex(SHA1BytesCodecExtensionMethods.encodeAsSHA1Bytes(theTarget))
}

static decodeSHA1(theTarget) {
static Object decodeSHA1(Object theTarget) {
throw new UnsupportedOperationException('Cannot decode SHA-1 hashes')
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,20 @@ package org.grails.plugins.codecs

import org.codehaus.groovy.runtime.NullObject

import groovy.transform.CompileStatic

@CompileStatic
class SHA256BytesCodecExtensionMethods {

// Returns the byte[] of the digest
static encodeAsSHA256Bytes(theTarget) {
static Object encodeAsSHA256Bytes(Object theTarget) {
if (theTarget == null || theTarget instanceof NullObject) {
return null
}
DigestUtils.digest('SHA-256', theTarget)
}

static decodeSHA256Bytes(theTarget) {
static Object decodeSHA256Bytes(Object theTarget) {
throw new UnsupportedOperationException('Cannot decode SHA-256 hashes')
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,20 @@ package org.grails.plugins.codecs

import org.codehaus.groovy.runtime.NullObject

import groovy.transform.CompileStatic

@CompileStatic
class SHA256CodecExtensionMethods extends DigestUtils {

// Returns the byte[] of the digest
static encodeAsSHA256(theTarget) {
static Object encodeAsSHA256(Object theTarget) {
if (theTarget == null || theTarget instanceof NullObject) {
return null
}
theTarget.encodeAsSHA256Bytes()?.encodeAsHex()
return HexCodecExtensionMethods.encodeAsHex(SHA256BytesCodecExtensionMethods.encodeAsSHA256Bytes(theTarget))
}

static decodeSHA256(theTarget) {
static Object decodeSHA256(Object theTarget) {
throw new UnsupportedOperationException('Cannot decode SHA-256 hashes')
}
}
Loading
Loading