diff --git a/ext/java/org/jruby/ext/digest/CRC32.java b/ext/java/org/jruby/ext/digest/CRC32.java new file mode 100644 index 0000000..7ac38ee --- /dev/null +++ b/ext/java/org/jruby/ext/digest/CRC32.java @@ -0,0 +1,127 @@ +/* + **** BEGIN LICENSE BLOCK ***** + * BSD 2-Clause License + * + * Copyright (c) 2026, Olle Jonsson + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ***** END LICENSE BLOCK *****/ + +package org.jruby.ext.digest; + +import java.io.IOException; + +import org.jruby.Ruby; +import org.jruby.RubyClass; +import org.jruby.RubyFixnum; +import org.jruby.RubyModule; +import org.jruby.RubyObject; +import org.jruby.RubyString; +import org.jruby.anno.JRubyClass; +import org.jruby.anno.JRubyMethod; +import org.jruby.api.Access; +import org.jruby.runtime.ThreadContext; +import org.jruby.runtime.Visibility; +import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.runtime.load.Library; +import org.jruby.util.ByteList; + +public class CRC32 implements Library { + + public void load(final Ruby runtime, boolean wrap) throws IOException { + runtime.getLoadService().require("digest"); + ThreadContext context = runtime.getCurrentContext(); + RubyModule Digest = Access.getModule(context, "Digest"); + RubyClass Base = Digest.getClass(context, "Base"); + RubyClass crc32 = Digest.defineClassUnder(context, "CRC32", Base, DigestCRC32::new); + crc32.defineMethods(context, DigestCRC32.class); + } + + private static final class CloneableCRC32 extends java.util.zip.CRC32 implements Cloneable { + @Override + public CloneableCRC32 clone() throws CloneNotSupportedException { + return (CloneableCRC32) super.clone(); + } + } + + @JRubyClass(name="Digest::CRC32", parent="Digest::Base") + public static class DigestCRC32 extends RubyObject { + private static final long serialVersionUID = -2811470471354871234L; + + private transient CloneableCRC32 crc = new CloneableCRC32(); + + public DigestCRC32(Ruby runtime, RubyClass type) { + super(runtime, type); + } + + @JRubyMethod(required = 1, visibility = Visibility.PRIVATE) + @Override + public IRubyObject initialize_copy(ThreadContext context, IRubyObject obj) { + if (this == obj) return this; + DigestCRC32 from = (DigestCRC32) obj; + this.checkFrozen(); + try { + this.crc = from.crc.clone(); + } catch (CloneNotSupportedException e) { + throw getRuntime().newRaiseException(getRuntime().getTypeError(), "Could not initialize copy of Digest::CRC32"); + } + return this; + } + + @JRubyMethod(name = {"update", "<<"}, required = 1) + public IRubyObject update(IRubyObject obj) { + ByteList bytes = obj.convertToString().getByteList(); + crc.update(bytes.getUnsafeBytes(), bytes.getBegin(), bytes.getRealSize()); + return this; + } + + @JRubyMethod() + public IRubyObject finish() { + long val = crc.getValue(); + crc.reset(); + byte[] digest = new byte[] { + (byte)(val >>> 24), + (byte)(val >>> 16), + (byte)(val >>> 8), + (byte) val + }; + return RubyString.newStringNoCopy(getRuntime(), digest); + } + + @JRubyMethod() + public IRubyObject reset() { + crc.reset(); + return this; + } + + @JRubyMethod() + public IRubyObject digest_length() { + return RubyFixnum.newFixnum(getRuntime(), 4); + } + + @JRubyMethod() + public IRubyObject block_length() { + return RubyFixnum.newFixnum(getRuntime(), 8); + } + } +} diff --git a/ext/java/org/jruby/ext/digest/lib/digest/crc32.rb b/ext/java/org/jruby/ext/digest/lib/digest/crc32.rb new file mode 100644 index 0000000..186cea2 --- /dev/null +++ b/ext/java/org/jruby/ext/digest/lib/digest/crc32.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +require "digest.jar" +JRuby::Util.load_ext("org.jruby.ext.digest.CRC32") diff --git a/test/digest/test_digest.rb b/test/digest/test_digest.rb index 95cce93..8a39a7b 100644 --- a/test/digest/test_digest.rb +++ b/test/digest/test_digest.rb @@ -204,7 +204,80 @@ class TestCRC32 < Test::Unit::TestCase Data1 => "352441c2", Data2 => "171a3f5f", } - end if defined?(Digest::CRC32) + + def test_digest_byte_order + # CRC32("abc") = 0x352441C2; raw digest bytes must be big-endian (MSB first) + assert_equal [0x35, 0x24, 0x41, 0xC2], Digest::CRC32.digest("abc").bytes + end + + def test_digest_length + assert_equal 4, Digest::CRC32.new.digest_length + end + + def test_block_length + assert_equal 8, Digest::CRC32.new.block_length + end + + def test_empty_string + assert_equal "00000000", Digest::CRC32.hexdigest("") + end + + def test_single_null_byte + assert_equal "d202ef8d", Digest::CRC32.hexdigest("\x00".b) + end + + def test_high_bytes + # Exercises the unsigned byte masking (& 0xFF) in the update loop + assert_equal "08eaaf6d", Digest::CRC32.hexdigest("\xFF\xFE\xFD".b) + end + + def test_all_byte_values + # Exercises every entry in the lookup table + assert_equal "29058c73", Digest::CRC32.hexdigest((0..255).map(&:chr).join.b) + end + + def test_incremental_equals_one_shot + inc = Digest::CRC32.new + inc << "a" << "b" << "c" + assert_equal "352441c2", inc.hexdigest + end + + def test_clone_mid_stream_independence + d = Digest::CRC32.new + d << "ab" + copy = d.clone + d << "c" + copy << "c" + assert_equal d.hexdigest, copy.hexdigest + end + + def test_reset + d = Digest::CRC32.new + d << "abc" + d.reset + d << "abc" + assert_equal "352441c2", d.hexdigest + end + + def test_digest_is_non_destructive + d = Digest::CRC32.new + d << "abc" + assert_equal d.hexdigest, d.hexdigest + end + + def test_digest_bang_resets_state + d = Digest::CRC32.new + d << "abc" + d.hexdigest! + assert_equal "00000000", d.hexdigest + end + + def test_initialize_copy_into_frozen_raises + dest = Digest::CRC32.allocate + dest.freeze + assert_raise(FrozenError) { dest.send(:initialize_copy, Digest::CRC32.new) } + end + end class TestBase < Test::Unit::TestCase def test_base