Skip to content

Commit c59800c

Browse files
authored
Merge pull request #128 from olleolleolle/add-a-java-crc32
Support CRC32 in JRuby
2 parents 1d77b3a + 03d3764 commit c59800c

3 files changed

Lines changed: 205 additions & 1 deletion

File tree

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
**** BEGIN LICENSE BLOCK *****
3+
* BSD 2-Clause License
4+
*
5+
* Copyright (c) 2026, Olle Jonsson
6+
* All rights reserved.
7+
*
8+
* Redistribution and use in source and binary forms, with or without
9+
* modification, are permitted provided that the following conditions are met:
10+
*
11+
* 1. Redistributions of source code must retain the above copyright notice, this
12+
* list of conditions and the following disclaimer.
13+
*
14+
* 2. Redistributions in binary form must reproduce the above copyright notice,
15+
* this list of conditions and the following disclaimer in the documentation
16+
* and/or other materials provided with the distribution.
17+
*
18+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28+
***** END LICENSE BLOCK *****/
29+
30+
package org.jruby.ext.digest;
31+
32+
import java.io.IOException;
33+
34+
import org.jruby.Ruby;
35+
import org.jruby.RubyClass;
36+
import org.jruby.RubyFixnum;
37+
import org.jruby.RubyModule;
38+
import org.jruby.RubyObject;
39+
import org.jruby.RubyString;
40+
import org.jruby.anno.JRubyClass;
41+
import org.jruby.anno.JRubyMethod;
42+
import org.jruby.api.Access;
43+
import org.jruby.runtime.ThreadContext;
44+
import org.jruby.runtime.Visibility;
45+
import org.jruby.runtime.builtin.IRubyObject;
46+
import org.jruby.runtime.load.Library;
47+
import org.jruby.util.ByteList;
48+
49+
public class CRC32 implements Library {
50+
51+
public void load(final Ruby runtime, boolean wrap) throws IOException {
52+
runtime.getLoadService().require("digest");
53+
ThreadContext context = runtime.getCurrentContext();
54+
RubyModule Digest = Access.getModule(context, "Digest");
55+
RubyClass Base = Digest.getClass(context, "Base");
56+
RubyClass crc32 = Digest.defineClassUnder(context, "CRC32", Base, DigestCRC32::new);
57+
crc32.defineMethods(context, DigestCRC32.class);
58+
}
59+
60+
private static final class CloneableCRC32 extends java.util.zip.CRC32 implements Cloneable {
61+
@Override
62+
public CloneableCRC32 clone() throws CloneNotSupportedException {
63+
return (CloneableCRC32) super.clone();
64+
}
65+
}
66+
67+
@JRubyClass(name="Digest::CRC32", parent="Digest::Base")
68+
public static class DigestCRC32 extends RubyObject {
69+
private static final long serialVersionUID = -2811470471354871234L;
70+
71+
private transient CloneableCRC32 crc = new CloneableCRC32();
72+
73+
public DigestCRC32(Ruby runtime, RubyClass type) {
74+
super(runtime, type);
75+
}
76+
77+
@JRubyMethod(required = 1, visibility = Visibility.PRIVATE)
78+
@Override
79+
public IRubyObject initialize_copy(ThreadContext context, IRubyObject obj) {
80+
if (this == obj) return this;
81+
DigestCRC32 from = (DigestCRC32) obj;
82+
this.checkFrozen();
83+
try {
84+
this.crc = from.crc.clone();
85+
} catch (CloneNotSupportedException e) {
86+
throw getRuntime().newRaiseException(getRuntime().getTypeError(), "Could not initialize copy of Digest::CRC32");
87+
}
88+
return this;
89+
}
90+
91+
@JRubyMethod(name = {"update", "<<"}, required = 1)
92+
public IRubyObject update(IRubyObject obj) {
93+
ByteList bytes = obj.convertToString().getByteList();
94+
crc.update(bytes.getUnsafeBytes(), bytes.getBegin(), bytes.getRealSize());
95+
return this;
96+
}
97+
98+
@JRubyMethod()
99+
public IRubyObject finish() {
100+
long val = crc.getValue();
101+
crc.reset();
102+
byte[] digest = new byte[] {
103+
(byte)(val >>> 24),
104+
(byte)(val >>> 16),
105+
(byte)(val >>> 8),
106+
(byte) val
107+
};
108+
return RubyString.newStringNoCopy(getRuntime(), digest);
109+
}
110+
111+
@JRubyMethod()
112+
public IRubyObject reset() {
113+
crc.reset();
114+
return this;
115+
}
116+
117+
@JRubyMethod()
118+
public IRubyObject digest_length() {
119+
return RubyFixnum.newFixnum(getRuntime(), 4);
120+
}
121+
122+
@JRubyMethod()
123+
public IRubyObject block_length() {
124+
return RubyFixnum.newFixnum(getRuntime(), 8);
125+
}
126+
}
127+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# frozen_string_literal: true
2+
3+
require "digest.jar"
4+
JRuby::Util.load_ext("org.jruby.ext.digest.CRC32")

test/digest/test_digest.rb

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,80 @@ class TestCRC32 < Test::Unit::TestCase
204204
Data1 => "352441c2",
205205
Data2 => "171a3f5f",
206206
}
207-
end if defined?(Digest::CRC32)
207+
208+
def test_digest_byte_order
209+
# CRC32("abc") = 0x352441C2; raw digest bytes must be big-endian (MSB first)
210+
assert_equal [0x35, 0x24, 0x41, 0xC2], Digest::CRC32.digest("abc").bytes
211+
end
212+
213+
def test_digest_length
214+
assert_equal 4, Digest::CRC32.new.digest_length
215+
end
216+
217+
def test_block_length
218+
assert_equal 8, Digest::CRC32.new.block_length
219+
end
220+
221+
def test_empty_string
222+
assert_equal "00000000", Digest::CRC32.hexdigest("")
223+
end
224+
225+
def test_single_null_byte
226+
assert_equal "d202ef8d", Digest::CRC32.hexdigest("\x00".b)
227+
end
228+
229+
def test_high_bytes
230+
# Exercises the unsigned byte masking (& 0xFF) in the update loop
231+
assert_equal "08eaaf6d", Digest::CRC32.hexdigest("\xFF\xFE\xFD".b)
232+
end
233+
234+
def test_all_byte_values
235+
# Exercises every entry in the lookup table
236+
assert_equal "29058c73", Digest::CRC32.hexdigest((0..255).map(&:chr).join.b)
237+
end
238+
239+
def test_incremental_equals_one_shot
240+
inc = Digest::CRC32.new
241+
inc << "a" << "b" << "c"
242+
assert_equal "352441c2", inc.hexdigest
243+
end
244+
245+
def test_clone_mid_stream_independence
246+
d = Digest::CRC32.new
247+
d << "ab"
248+
copy = d.clone
249+
d << "c"
250+
copy << "c"
251+
assert_equal d.hexdigest, copy.hexdigest
252+
end
253+
254+
def test_reset
255+
d = Digest::CRC32.new
256+
d << "abc"
257+
d.reset
258+
d << "abc"
259+
assert_equal "352441c2", d.hexdigest
260+
end
261+
262+
def test_digest_is_non_destructive
263+
d = Digest::CRC32.new
264+
d << "abc"
265+
assert_equal d.hexdigest, d.hexdigest
266+
end
267+
268+
def test_digest_bang_resets_state
269+
d = Digest::CRC32.new
270+
d << "abc"
271+
d.hexdigest!
272+
assert_equal "00000000", d.hexdigest
273+
end
274+
275+
def test_initialize_copy_into_frozen_raises
276+
dest = Digest::CRC32.allocate
277+
dest.freeze
278+
assert_raise(FrozenError) { dest.send(:initialize_copy, Digest::CRC32.new) }
279+
end
280+
end
208281

209282
class TestBase < Test::Unit::TestCase
210283
def test_base

0 commit comments

Comments
 (0)