diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5dbc2d7..61cac17 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,3 +41,17 @@ jobs: - name: Run test run: bundle exec rake compile test timeout-minutes: 5 + + build-jruby: + name: build jruby + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: jruby-10 + bundler-cache: true # 'bundle install' and cache + - name: Run test + run: bundle exec rake compile test + timeout-minutes: 5 diff --git a/.gitignore b/.gitignore index cc4b63a..a495cdb 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ *.bundle *.so *.dll +/ext/java/lib/zlib.jar diff --git a/Gemfile b/Gemfile index c39582a..c09ba21 100644 --- a/Gemfile +++ b/Gemfile @@ -7,3 +7,4 @@ gem "test-unit" gem "test-unit-ruby-core" gem "rake" gem "rake-compiler" +gem "ruby-maven", platform: :jruby diff --git a/Mavenfile b/Mavenfile new file mode 100644 index 0000000..ae3202b --- /dev/null +++ b/Mavenfile @@ -0,0 +1,7 @@ +#-*- mode: ruby -*- + +jar 'org.jruby:jzlib:1.1.5' + +plugin :dependency, '2.8', :outputFile => 'pkg/classpath' + +# vim: syntax=Ruby diff --git a/Rakefile b/Rakefile index fdab5bb..92a8e3c 100644 --- a/Rakefile +++ b/Rakefile @@ -12,11 +12,31 @@ end Rake::TestTask.new(:test_internal) do |t| t.libs << "test/lib" + t.libs << "ext/java/lib" if RUBY_ENGINE == "jruby" t.ruby_opts << "-rhelper" t.test_files = FileList["test/**/test_*.rb"] end -require 'rake/extensiontask' -Rake::ExtensionTask.new("zlib") +if RUBY_PLATFORM =~ /java/ + require 'rake/javaextensiontask' + Rake::JavaExtensionTask.new("zlib") do |ext| + require 'maven/ruby/maven' + # force load of versions to overwrite constants with values from repo. + load './ext/java/lib/zlib/versions.rb' + # uses Mavenfile to write classpath into pkg/classpath + # and tell maven via system properties the snakeyaml version + # this is basically the same as running from the commandline: + # rmvn dependency:build-classpath -Dsnakeyaml.version='use version from Psych::DEFAULT_SNAKEYAML_VERSION here' + Maven::Ruby::Maven.new.exec('dependency:build-classpath', '-Dverbose=true') + ext.source_version = '21' + ext.target_version = '21' + ext.classpath = File.read('pkg/classpath') + ext.ext_dir = 'ext/java' + ext.lib_dir = 'ext/java/lib' + end +else + require 'rake/extensiontask' + Rake::ExtensionTask.new("zlib") +end task :default => [:compile, :test] diff --git a/ext/java/lib/zlib.rb b/ext/java/lib/zlib.rb new file mode 100644 index 0000000..8acb0ea --- /dev/null +++ b/ext/java/lib/zlib.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'zlib.jar' +require 'zlib/versions' + +require 'jar-dependencies' +require_jar('org.jruby', 'jzlib', Zlib::JZLIB_VERSION) + +JRuby::Util.load_ext('org.jruby.ext.zlib.ZlibLibrary') + +module Zlib + autoload :StringIO, 'stringio' + + def self.gzip(src, opts = nil) + if Hash === opts + level = opts[:level] + strategy = opts[:strategy] + end + io = StringIO.new("".b) + GzipWriter.new(io, level, strategy) do |writer| + writer.write(src) + end + io.string + end + + def self.gunzip(src) + io = StringIO.new(src) + reader = GzipReader.new(io) + result = reader.read + reader.close + result + end +end diff --git a/ext/java/lib/zlib/versions.rb b/ext/java/lib/zlib/versions.rb new file mode 100644 index 0000000..e052d28 --- /dev/null +++ b/ext/java/lib/zlib/versions.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module Zlib + JZLIB_VERSION = "1.1.5" +end \ No newline at end of file diff --git a/ext/java/org/jruby/ext/zlib/JZlibDeflate.java b/ext/java/org/jruby/ext/zlib/JZlibDeflate.java new file mode 100644 index 0000000..8c89cd5 --- /dev/null +++ b/ext/java/org/jruby/ext/zlib/JZlibDeflate.java @@ -0,0 +1,315 @@ +/* + **** BEGIN LICENSE BLOCK ***** + * Version: EPL 2.0/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Eclipse Public + * License Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.eclipse.org/legal/epl-v20.html + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing + * rights and limitations under the License. + * + * Alternatively, the contents of this file may be used under the terms of + * either of the GNU General Public License Version 2 or later (the "GPL"), + * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the EPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the EPL, the GPL or the LGPL. + ***** END LICENSE BLOCK *****/ + +package org.jruby.ext.zlib; + +import com.jcraft.jzlib.JZlib; +import java.io.IOException; +import org.jruby.Ruby; +import org.jruby.RubyBasicObject; +import org.jruby.RubyClass; +import org.jruby.RubyNumeric; +import org.jruby.RubyString; +import org.jruby.anno.JRubyClass; +import org.jruby.anno.JRubyMethod; +import org.jruby.runtime.Arity; +import org.jruby.runtime.Block; +import org.jruby.runtime.ThreadContext; +import org.jruby.runtime.builtin.IRubyObject; + +import static org.jruby.api.Convert.toInt; +import static org.jruby.api.Create.newString; +import static org.jruby.api.Error.typeError; +import static org.jruby.runtime.Visibility.PRIVATE; +import org.jruby.util.ByteList; + +@JRubyClass(name = "Zlib::Deflate", parent = "Zlib::ZStream") +public class JZlibDeflate extends ZStream { + public static final int BASE_SIZE = 100; + + private int level; + private int windowBits; + private int strategy; + private byte[] collected; + private int collectedIdx; + private com.jcraft.jzlib.Deflater flater = null; + private int flush = JZlib.Z_NO_FLUSH; + + @Deprecated(since = "9.4") + public static IRubyObject s_deflate(IRubyObject recv, IRubyObject[] args) { + return s_deflate(((RubyBasicObject) recv).getCurrentContext(), recv, args); + } + + @JRubyMethod(name = "deflate", required = 1, optional = 1, checkArity = false, meta = true) + public static IRubyObject s_deflate(ThreadContext context, IRubyObject recv, IRubyObject[] args) { + args = Arity.scanArgs(context, args, 1, 1); + int level = JZlib.Z_DEFAULT_COMPRESSION; + if (!args[1].isNil()) level = checkLevel(context, toInt(context, args[1])); + + RubyClass klass = (RubyClass)(recv.isClass() ? recv : context.runtime.getClassFromPath("Zlib::Deflate")); + JZlibDeflate deflate = (JZlibDeflate) klass.allocate(context); + deflate.init(context, level, JZlib.DEF_WBITS, 8, JZlib.Z_DEFAULT_STRATEGY); + + try { + IRubyObject result = deflate.deflate(context, args[0].convertToString().getByteList(), JZlib.Z_FINISH); + deflate.close(context); + return result; + } catch (IOException ioe) { + throw context.runtime.newIOErrorFromException(ioe); + } + } + + public JZlibDeflate(Ruby runtime, RubyClass type) { + super(runtime, type); + } + + @Deprecated + public IRubyObject _initialize(IRubyObject[] args) { + return _initialize(getCurrentContext(), args); + } + + @JRubyMethod(name = "initialize", optional = 4, checkArity = false, visibility = PRIVATE) + public IRubyObject _initialize(ThreadContext context, IRubyObject[] args) { + args = Arity.scanArgs(context, args, 0, 4); + level = !args[0].isNil() ? checkLevel(context, toInt(context, args[0])) : -1; + windowBits = !args[1].isNil() ? checkWindowBits(context, toInt(context, args[1]), false) : JZlib.MAX_WBITS; + int memlevel = !args[2].isNil() ? toInt(context, args[2]) : 8; // ignored. Memory setting means nothing on Java. + strategy = !args[3].isNil() ? toInt(context, args[3]) : 0; + + init(context, level, windowBits, memlevel, strategy); + return this; + } + + private void init(ThreadContext context, int level, int windowBits, int memlevel, int strategy) { + flush = JZlib.Z_NO_FLUSH; + flater = new com.jcraft.jzlib.Deflater(); + + // TODO: Can we expect JZlib to check level, windowBits, and strategy here? + // Then we should remove checkLevel, checkWindowsBits and checkStrategy. + int err = flater.init(level, windowBits, memlevel); + if (err == com.jcraft.jzlib.JZlib.Z_STREAM_ERROR) throw RubyZlib.newStreamError(context, "stream error"); + + err = flater.params(level, strategy); + if (err == com.jcraft.jzlib.JZlib.Z_STREAM_ERROR) throw RubyZlib.newStreamError(context, "stream error"); + + collected = new byte[BASE_SIZE]; + collectedIdx = 0; + } + + @JRubyMethod(visibility = PRIVATE) + public IRubyObject initialize_copy(ThreadContext context, IRubyObject _other) { + if (!(_other instanceof JZlibDeflate other)) throw typeError(context, "Expecting an instance of class JZlibDeflate"); + if (this == _other) return this; + + this.level = other.level; + this.windowBits = other.windowBits; + this.strategy = other.strategy; + this.collected = new byte[other.collected.length]; + System.arraycopy(other.collected, 0, this.collected, 0, other.collected.length); + this.collectedIdx = other.collectedIdx; + + this.flush = other.flush; + this.flater = new com.jcraft.jzlib.Deflater(); + int ret = this.flater.copy(other.flater); + if (ret != com.jcraft.jzlib.JZlib.Z_OK) throw RubyZlib.newStreamError(context, "stream error"); + + return this; + } + + @Deprecated(since = "10.0") + public IRubyObject append(IRubyObject arg) { + return append(getCurrentContext(), arg); + } + + @JRubyMethod(name = "<<") + public IRubyObject append(ThreadContext context, IRubyObject arg) { + checkClosed(context); + try { + append(context, arg.convertToString().getByteList()); + } catch (IOException ioe) { + throw context.runtime.newIOErrorFromException(ioe); + } + return this; + } + + @JRubyMethod(name = "params") + public IRubyObject params(ThreadContext context, IRubyObject level, IRubyObject strategy) { + int l = toInt(context, level); + checkLevel(context, l); + + int s = toInt(context, strategy); + checkStrategy(context, s); + + if (flater.next_out == null) flater.setOutput(ByteList.NULL_ARRAY); + + int err = flater.params(l, s); + if (err == com.jcraft.jzlib.JZlib.Z_STREAM_ERROR) throw RubyZlib.newStreamError(context, "stream error"); + + if (collectedIdx != flater.next_out_index) collectedIdx = flater.next_out_index; + + run(context); + return context.nil; + } + + @JRubyMethod(name = "set_dictionary") + public IRubyObject set_dictionary(ThreadContext context, IRubyObject arg) { + try { + byte[] tmp = arg.convertToString().getBytes(); + int err = flater.setDictionary(tmp, tmp.length); + if (err == com.jcraft.jzlib.JZlib.Z_STREAM_ERROR) { + throw RubyZlib.newStreamError(context, "stream error: "); + } + run(context); + return arg; + } catch (IllegalArgumentException iae) { + throw RubyZlib.newStreamError(context, "stream error: " + iae.getMessage()); + } + } + + @Deprecated + public IRubyObject flush(IRubyObject[] args) { + return flush(getCurrentContext(), args); + } + + @JRubyMethod(name = "flush", optional = 1, checkArity = false) + public IRubyObject flush(ThreadContext context, IRubyObject[] args) { + Arity.checkArgumentCount(context, args, 0, 1); + + int flush = args.length == 1 && !args[0].isNil() ? toInt(context, args[0]) : 2; // SYNC_FLUSH + + return flush(context, flush); + } + + @Deprecated + public IRubyObject deflate(IRubyObject[] args) { + return deflate(getCurrentContext(), args); + } + + @JRubyMethod(name = "deflate", required = 1, optional = 1, checkArity = false) + public IRubyObject deflate(ThreadContext context, IRubyObject[] args) { + args = Arity.scanArgs(context, args, 1, 1); + if (internalFinished()) throw RubyZlib.newStreamError(context, "stream error"); + + ByteList data = !args[0].isNil() ? args[0].convertToString().getByteList() : null; + int flush = !args[1].isNil() ? toInt(context, args[1]) : JZlib.Z_NO_FLUSH; + + try { + return deflate(context, data, flush); + } catch (IOException ioe) { + throw context.runtime.newIOErrorFromException(ioe); + } + } + + @Override + protected int internalTotalIn() { + return (int) flater.total_in; + } + + @Override + protected int internalTotalOut() { + return (int) flater.total_out; + } + + @Override + protected boolean internalStreamEndP() { + return flater.finished(); + } + + @Override + protected void internalReset(ThreadContext context) { + init(context, level, windowBits, 8, strategy); + } + + @Override + public boolean internalFinished() { + return flater.finished(); + } + + @Override + protected long internalAdler() { + return flater.getAdler(); + } + + @Override + protected IRubyObject internalFinish(ThreadContext context, Block block) { + return finish(context); + } + + @Override + protected void internalClose() { + flater.end(); + } + + private void append(ThreadContext context, ByteList obj) throws IOException { + flater.setInput(obj.getUnsafeBytes(), obj.getBegin(), obj.getRealSize(), true); + run(context); + } + + private IRubyObject flush(ThreadContext context, int flush) { + int last_flush = this.flush; + this.flush = flush; + if (flush == JZlib.Z_NO_FLUSH) return RubyString.newEmptyBinaryString(context.runtime); + + run(context); + this.flush = last_flush; + IRubyObject obj = newString(context, collected, 0, collectedIdx); + collectedIdx = 0; + flater.setOutput(collected); + return obj; + } + + private IRubyObject deflate(ThreadContext context, ByteList str, int flush) throws IOException { + if (null != str) append(context, str); + + return flush(context, flush); + } + + private IRubyObject finish(ThreadContext context) { + return flush(context, JZlib.Z_FINISH); + } + + private void run(ThreadContext context) { + if (internalFinished()) return; + + while (!internalFinished()) { + flater.setOutput(collected, collectedIdx, collected.length - collectedIdx); + + int err = flater.deflate(flush); + if (err == com.jcraft.jzlib.JZlib.Z_STREAM_ERROR) throw RubyZlib.newStreamError(context, "stream error: "); + + if (collectedIdx == flater.next_out_index) break; + + collectedIdx = flater.next_out_index; + + if (collected.length == collectedIdx && !internalFinished()) { + byte[] tmp = new byte[collected.length * 3]; + System.arraycopy(collected, 0, tmp, 0, collected.length); + collected = tmp; + } + } + } +} diff --git a/ext/java/org/jruby/ext/zlib/JZlibInflate.java b/ext/java/org/jruby/ext/zlib/JZlibInflate.java new file mode 100644 index 0000000..f8183a7 --- /dev/null +++ b/ext/java/org/jruby/ext/zlib/JZlibInflate.java @@ -0,0 +1,343 @@ +/* + **** BEGIN LICENSE BLOCK ***** + * Version: EPL 2.0/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Eclipse Public + * License Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.eclipse.org/legal/epl-v20.html + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing + * rights and limitations under the License. + * + * Alternatively, the contents of this file may be used under the terms of + * either of the GNU General Public License Version 2 or later (the "GPL"), + * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the EPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the EPL, the GPL or the LGPL. + ***** END LICENSE BLOCK *****/ + +package org.jruby.ext.zlib; + +import com.jcraft.jzlib.JZlib; +import org.jruby.*; +import org.jruby.anno.JRubyClass; +import org.jruby.anno.JRubyMethod; +import org.jruby.runtime.Arity; +import org.jruby.runtime.Block; +import org.jruby.runtime.ObjectAllocator; +import org.jruby.runtime.ThreadContext; + +import static org.jruby.api.Convert.asFixnum; +import static org.jruby.api.Convert.toInt; +import static org.jruby.api.Create.newString; +import static org.jruby.runtime.Visibility.PRIVATE; +import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.util.ByteList; + + @JRubyClass(name = "Zlib::Inflate", parent = "Zlib::ZStream") +public class JZlibInflate extends ZStream { + + public static final int BASE_SIZE = 100; + private int windowBits; + private byte[] collected; + private int collectedIdx; + private ByteList input; + private com.jcraft.jzlib.Inflater flater = null; + + public JZlibInflate(Ruby runtime, RubyClass type) { + super(runtime, type); + } + + @JRubyMethod(name = "inflate", meta = true) + public static IRubyObject s_inflate(ThreadContext context, IRubyObject recv, IRubyObject string) { + RubyClass klass = (RubyClass)(recv.isClass() ? recv : context.runtime.getClassFromPath("Zlib::Inflate")); + JZlibInflate inflate = (JZlibInflate) klass.allocate(context); + inflate.init(JZlib.DEF_WBITS); + + try { + return inflate.inflate(context, string, Block.NULL_BLOCK); + } finally { + inflate.finish(context, Block.NULL_BLOCK); + inflate.close(context); + } + } + + @Deprecated + public IRubyObject _initialize(IRubyObject[] args) { + return _initialize(getCurrentContext(), args); + } + + @JRubyMethod(name = "initialize", optional = 1, checkArity = false, visibility = PRIVATE) + public IRubyObject _initialize(ThreadContext context, IRubyObject[] args) { + int argc = Arity.checkArgumentCount(context, args, 0, 1); + + windowBits = argc > 0 && !args[0].isNil() ? + checkWindowBits(context, toInt(context, args[0]), true) : JZlib.DEF_WBITS; + + init(windowBits); + return this; + } + + private void init(int windowBits) { + flater = new com.jcraft.jzlib.Inflater(); + flater.init(windowBits); + collected = new byte[BASE_SIZE]; + collectedIdx = 0; + input = new ByteList(); + } + + @Override + @JRubyMethod(name = "flush_next_out") + public IRubyObject flush_next_out(ThreadContext context, Block block) { + return flushOutput(context, block); + } + + private IRubyObject flushOutput(ThreadContext context, Block block) { + if (collectedIdx > 0) { + RubyString res = newString(context, collected, 0, collectedIdx); + collectedIdx = 0; + flater.setOutput(collected); + + if (block.isGiven()) { + block.yield(context, res); + return context.nil; + } + return res; + } + return RubyString.newEmptyBinaryString(context.runtime); + } + + @JRubyMethod(name = "<<") + public IRubyObject append(ThreadContext context, IRubyObject arg) { + checkClosed(context); + if (arg.isNil()) { + run(context, true); + } else { + append(context, arg.convertToString().getByteList()); + } + return this; + } + + @Deprecated(since = "10.0") + public void append(ByteList obj) { + append(getCurrentContext(), obj); + } + + public void append(ThreadContext context, ByteList obj) { + if (!internalFinished()) { + flater.setInput(obj.bytes(), true); + } else { + input.append(obj); + } + run(context, false); + } + + @Deprecated(since = "10.0") + public IRubyObject sync_point_p() { + return sync_point_p(getCurrentContext()); + } + + @JRubyMethod(name = "sync_point?") + public IRubyObject sync_point_p(ThreadContext context) { + return sync_point(context); + } + + @Deprecated(since = "10.0") + public IRubyObject sync_point() { + return sync_point(getCurrentContext()); + } + + public IRubyObject sync_point(ThreadContext context) { + int ret = flater.syncPoint(); + return switch (ret) { + case 1 -> context.tru; + case JZlib.Z_DATA_ERROR -> throw RubyZlib.newStreamError(context, "stream error"); + default -> context.fals; + }; + } + + @JRubyMethod(name = "set_dictionary") + public IRubyObject set_dictionary(ThreadContext context, IRubyObject arg) { + try { + byte[] tmp = arg.convertToString().getBytes(); + int ret = flater.setDictionary(tmp, tmp.length); + switch (ret) { + case JZlib.Z_STREAM_ERROR -> throw RubyZlib.newStreamError(context, "stream error"); + case JZlib.Z_DATA_ERROR -> throw RubyZlib.newDataError(context, "wrong dictionary"); + } + run(context, false); + return arg; + } catch (IllegalArgumentException iae) { + throw RubyZlib.newStreamError(context, "stream error: " + iae.getMessage()); + } + } + + @JRubyMethod(name = "inflate") + public IRubyObject inflate(ThreadContext context, IRubyObject string, Block block) { + ByteList data = null; + if (!string.isNil()) { + data = string.convertToString().getByteList(); + } + return inflate(context, data, block); + } + + public IRubyObject inflate(ThreadContext context, ByteList str, Block block) { + if (str == null) return internalFinish(context, block); + + append(context, str); + return flushOutput(context, block); + } + + @JRubyMethod(name = "sync") + public IRubyObject sync(ThreadContext context, IRubyObject string) { + if (flater.avail_in > 0) { + switch (flater.sync()) { + case JZlib.Z_OK -> { + flater.setInput(string.convertToString().getByteList().bytes(), true); + return context.tru; + } + case JZlib.Z_DATA_ERROR -> {} + default -> throw RubyZlib.newStreamError(context, "stream error"); + } + } + if (string.convertToString().getByteList().length() <= 0) return context.fals; + + flater.setInput(string.convertToString().getByteList().bytes(), true); + switch (flater.sync()) { + case com.jcraft.jzlib.JZlib.Z_OK: + return context.tru; + case com.jcraft.jzlib.JZlib.Z_DATA_ERROR: + return context.fals; + default: + throw RubyZlib.newStreamError(context, "stream error"); + } + } + + private void run(ThreadContext context, boolean finish) { + int resultLength = -1; + + while (!internalFinished() && resultLength != 0) { + // MRI behavior + boolean needsInput = flater.avail_in < 0; + if (finish && needsInput) { + throw RubyZlib.newBufError(context, "buffer error"); + } + + flater.setOutput(collected, collectedIdx, collected.length - collectedIdx); + int ret = flater.inflate(com.jcraft.jzlib.JZlib.Z_NO_FLUSH); + resultLength = flater.next_out_index - collectedIdx; + collectedIdx = flater.next_out_index; + switch (ret) { + case com.jcraft.jzlib.JZlib.Z_DATA_ERROR: + /* + * resultLength = flater.next_out_index; if(resultLength>0){ + * // error has been occurred, // but some data has been + * inflated successfully. collected.append(outp, 0, + * resultLength); } + */ + throw RubyZlib.newDataError(context, flater.getMessage()); + case com.jcraft.jzlib.JZlib.Z_NEED_DICT: + throw RubyZlib.newDictError(context, "need dictionary"); + case com.jcraft.jzlib.JZlib.Z_STREAM_END: + if (flater.avail_in > 0) { + // MRI behavior: pass-through + input.append(flater.next_in, + flater.next_in_index, flater.avail_in); + flater.setInput("".getBytes()); + } + case com.jcraft.jzlib.JZlib.Z_OK: + resultLength = flater.next_out_index; + break; + default: + resultLength = 0; + } + if (collected.length == collectedIdx && !internalFinished()) { + byte[] tmp = new byte[collected.length * 3]; + System.arraycopy(collected, 0, tmp, 0, collected.length); + collected = tmp; + } + } + if (finish) { + if (!internalFinished()) { + int err = flater.inflate(com.jcraft.jzlib.JZlib.Z_FINISH); + if (err != com.jcraft.jzlib.JZlib.Z_OK) { + throw RubyZlib.newBufError(context, "buffer error"); + } + } + } + } + + @Override + protected int internalTotalIn() { + return (int) flater.total_in; + } + + @Override + protected int internalTotalOut() { + return (int) flater.total_out; + } + + @Override + protected boolean internalStreamEndP() { + return flater.finished(); + } + + @Override + protected void internalReset(ThreadContext context) { + init(windowBits); + } + + @Override + protected boolean internalFinished() { + return flater.finished(); + } + + @Override + protected long internalAdler() { + return flater.getAdler(); + } + + @Override + protected IRubyObject internalFinish(ThreadContext context, Block block) { + run(context, true); + // MRI behavior: in finished mode, we work as pass-through + if (internalFinished()) { + if (input.getRealSize() > 0) { + if (collected.length - collectedIdx < input.length()) { + byte[] tmp = new byte[collected.length + input.length()]; + System.arraycopy(collected, 0, tmp, 0, collectedIdx); + collected = tmp; + } + System.arraycopy(input.getUnsafeBytes(), input.begin(), collected, collectedIdx, input.length()); + collectedIdx += input.length(); + resetBuffer(input); + } + } + return flushOutput(context, block); + } + + @Override + protected void internalClose() { + flater.end(); + } + + @Override + public IRubyObject avail_in(ThreadContext context) { + return asFixnum(context, flater.avail_in); + } + + private static void resetBuffer(ByteList l) { + l.setBegin(0); + l.setRealSize(0); + l.invalidate(); + } +} \ No newline at end of file diff --git a/ext/java/org/jruby/ext/zlib/JZlibRubyGzipReader.java b/ext/java/org/jruby/ext/zlib/JZlibRubyGzipReader.java new file mode 100644 index 0000000..e276bdf --- /dev/null +++ b/ext/java/org/jruby/ext/zlib/JZlibRubyGzipReader.java @@ -0,0 +1,859 @@ +/* + **** BEGIN LICENSE BLOCK ***** + * Version: EPL 2.0/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Eclipse Public + * License Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.eclipse.org/legal/epl-v20.html + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing + * rights and limitations under the License. + * + * Alternatively, the contents of this file may be used under the terms of + * either of the GNU General Public License Version 2 or later (the "GPL"), + * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the EPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the EPL, the GPL or the LGPL. + ***** END LICENSE BLOCK *****/ + +package org.jruby.ext.zlib; + +import com.jcraft.jzlib.GZIPException; +import com.jcraft.jzlib.GZIPInputStream; +import com.jcraft.jzlib.Inflater; +import org.jruby.Ruby; +import org.jruby.RubyBasicObject; +import org.jruby.RubyClass; +import org.jruby.RubyEnumerator; +import org.jruby.RubyException; +import org.jruby.RubyInteger; +import org.jruby.RubyNumeric; +import org.jruby.RubyString; +import org.jruby.anno.FrameField; +import org.jruby.anno.JRubyClass; +import org.jruby.anno.JRubyMethod; +import org.jruby.exceptions.RaiseException; +import org.jruby.runtime.Arity; +import org.jruby.runtime.Block; +import org.jruby.runtime.Helpers; +import org.jruby.runtime.ThreadContext; +import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.util.ByteList; +import org.jruby.util.IOInputStream; +import org.jruby.util.StringSupport; +import org.jruby.util.TypeConverter; +import org.jruby.util.io.EncodingUtils; +import org.jruby.util.io.PosixShim; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.PushbackInputStream; +import java.util.ArrayList; +import java.util.List; + +import static org.jruby.RubyIO.PARAGRAPH_SEPARATOR; +import static org.jruby.api.Access.fileClass; +import static org.jruby.api.Access.globalVariables; +import static org.jruby.api.Convert.asFixnum; +import static org.jruby.api.Convert.castAsString; +import static org.jruby.api.Convert.toInt; +import static org.jruby.api.Convert.toLong; +import static org.jruby.api.Create.*; +import static org.jruby.api.Error.argumentError; +import static org.jruby.runtime.Visibility.PRIVATE; + +@JRubyClass(name = "Zlib::GzipReader", parent = "Zlib::GzipFile", include = "Enumerable") +public class JZlibRubyGzipReader extends RubyGzipFile { + @JRubyClass(name = "Zlib::GzipReader::Error", parent = "Zlib::GzipReader") + public static class Error {} + + @JRubyMethod(name = "new", rest = true, meta = true, keywords = true) + public static IRubyObject newInstance(ThreadContext context, IRubyObject recv, IRubyObject[] args, Block block) { + JZlibRubyGzipReader result = newInstance(context, (RubyClass) recv, args); + + return RubyGzipFile.wrapBlock(context, result, block); + } + + @Deprecated(since = "10.0") + public static JZlibRubyGzipReader newInstance(IRubyObject recv, IRubyObject[] args) { + return newInstance(((RubyBasicObject) recv).getCurrentContext(), (RubyClass) recv, args); + } + + public static JZlibRubyGzipReader newInstance(ThreadContext context, RubyClass klass, IRubyObject[] args) { + JZlibRubyGzipReader result = (JZlibRubyGzipReader) klass.allocate(context); + + result.callInit(args, Block.NULL_BLOCK); + + return result; + } + + @JRubyMethod(name = "open", required = 1, optional = 1, checkArity = false, meta = true) + public static IRubyObject open(final ThreadContext context, IRubyObject recv, IRubyObject[] args, Block block) { + Arity.checkArgumentCount(context, args, 1, 2); + + args[0] = Helpers.invoke(context, fileClass(context), "open", args[0], newString(context, "rb")); + + JZlibRubyGzipReader gzio = newInstance(context, (RubyClass) recv, args); + + return RubyGzipFile.wrapBlock(context, gzio, block); + } + + public JZlibRubyGzipReader(Ruby runtime, RubyClass type) { + super(runtime, type); + } + + public IRubyObject initialize(ThreadContext context, IRubyObject stream) { + realIo = stream; + + try { + // don't close realIO + ioInputStream = new IOInputStream(realIo); + io = new GZIPInputStream(ioInputStream, 512, false); + + // JRUBY-4502 + // CRuby expects to parse gzip header in 'new'. + io.readHeader(); + + } catch (IOException e) { + RaiseException re = RubyZlib.newGzipFileError(context, "not in gzip format"); + + byte[] input = io.getAvailIn(); + if (input != null && input.length > 0) { + RubyException rubye = re.getException(); + rubye.setInstanceVariable("@input", newString(context, new ByteList(input, 0, input.length))); + } + + throw re; + } + + position = 0; + line = 0; + bufferedStream = new PushbackInputStream(new BufferedInputStream(io), 512); + mtime = org.jruby.RubyTime.newTime(context.runtime, io.getModifiedTime() * 1000); + + return this; + } + + @JRubyMethod(name = "initialize", required = 1, optional = 1, checkArity = false, visibility = PRIVATE) + public IRubyObject initialize(ThreadContext context, IRubyObject[] args) { + int argc = Arity.checkArgumentCount(context, args, 1, 2); + IRubyObject obj = initialize(context, args[0]); + IRubyObject opt = context.nil; + + if (argc == 2) { + opt = args[1]; + if (TypeConverter.checkHashType(context.runtime, opt).isNil()) throw argumentError(context, 2, 1); + } + + ecopts(context, opt); + + return obj; + } + + /** + * Get position within this stream including that has been read by users + * calling read + what jzlib may have speculatively read in because of + * buffering. + * + * @return number of bytes + */ + private long internalPosition() { + Inflater inflater = io.getInflater(); + + return inflater.getTotalIn() + inflater.getAvailIn(); + } + + @JRubyMethod + public IRubyObject rewind(ThreadContext context) { + // should invoke seek on realIo... + realIo.callMethod(context, "seek", + new IRubyObject[]{asFixnum(context, -internalPosition()), asFixnum(context, PosixShim.SEEK_CUR)}); + + // ... and then reinitialize + initialize(context, realIo); + + return context.nil; + } + + @JRubyMethod(name = "lineno") + public IRubyObject lineno(ThreadContext context) { + return asFixnum(context, line); + } + + @Deprecated + public IRubyObject lineno() { + return lineno(getCurrentContext()); + } + + @JRubyMethod(name = "readline", writes = FrameField.LASTLINE) + public IRubyObject readline(ThreadContext context) { + IRubyObject dst = gets(context, IRubyObject.NULL_ARRAY); + + if (dst.isNil()) throw context.runtime.newEOFError(); + + return dst; + } + + private IRubyObject internalGets(ThreadContext context, IRubyObject[] args) throws IOException { + ByteList sep = ((RubyString) globalVariables(context).get("$/")).getByteList(); + int limit = -1; + + switch (args.length) { + case 0: + break; + case 1: + if (args[0].isNil()) return readAll(context); + + IRubyObject tmp = args[0].checkStringType(); + if (tmp.isNil()) { + limit = toInt(context, args[0]); + } else { + sep = tmp.convertToString().getByteList(); + } + break; + case 2: + default: + limit = toInt(context, args[1]); + if (args[0].isNil()) return readAll(context, limit); + + sep = args[0].convertToString().getByteList(); + break; + } + + return internalSepGets(context, sep, limit); + } + + private IRubyObject internalSepGets(ThreadContext context, ByteList sep) throws IOException { + return internalSepGets(context, sep, -1); + } + + private ByteList newReadByteList() { + ByteList byteList = new ByteList(); + + return byteList; + } + + private ByteList newReadByteList(int size) { + ByteList byteList = new ByteList(size); + + return byteList; + } + + private IRubyObject internalSepGets(ThreadContext context, ByteList sep, int limit) throws IOException { + ByteList result = newReadByteList(); + boolean stripNewlines = false; + + if (sep.getRealSize() == 0) { + sep = PARAGRAPH_SEPARATOR; + stripNewlines = true; + } + + if (stripNewlines) skipNewlines(); + + int ce = -1; + + while (limit <= 0 || result.length() < limit) { + int sepOffset = result.length() - sep.getRealSize(); + if (sepOffset >= 0 && result.startsWith(sep, sepOffset)) break; + + ce = bufferedStream.read(); + + if (ce == -1) break; + + result.append(ce); + } + + fixBrokenTrailingCharacter(context, result); + + if (stripNewlines) skipNewlines(); + + // io.available() only returns 0 after EOF is encountered + // so we need to differentiate between the empty string and EOF + if (0 == result.length() && -1 == ce) return context.nil; + + line++; + position += result.length(); + + return newStr(context, result); + } + + private static final int NEWLINE = '\n'; + + private void skipNewlines() throws IOException { + while (true) { + int b = bufferedStream.read(); + if (b == -1) break; + + if (b != NEWLINE) { + bufferedStream.unread(b); + break; + } + } + } + + @JRubyMethod(name = "gets", optional = 2, checkArity = false, writes = FrameField.LASTLINE) + public IRubyObject gets(ThreadContext context, IRubyObject[] args) { + try { + IRubyObject result = internalGets(context, args); + + if (!result.isNil()) context.setLastLine(result); + + return result; + } catch (IOException ioe) { + throw context.runtime.newIOErrorFromException(ioe); + } + } + private final static int BUFF_SIZE = 4096; + + @JRubyMethod(name = "read", optional = 1, checkArity = false) + public IRubyObject read(ThreadContext context, IRubyObject[] args) { + int argc = Arity.checkArgumentCount(context, args, 0, 1); + + try { + if (argc == 0 || args[0].isNil()) return readAll(context); + + int len = toInt(context, args[0]); + if (len < 0) throw argumentError(context, "negative length " + len + " given"); + if (len > 0) { // rb_gzfile_read + ByteList buf = readSize(len); + return buf == null ? context.nil : newString(context, buf); + } + + return RubyString.newEmptyBinaryString(context.runtime); + } catch (IOException ioe) { + String m = ioe.getMessage(); + + if (m.startsWith("Unexpected end of ZLIB input stream")) { + throw RubyZlib.newGzipFileError(context, ioe.getMessage()); + } else if (m.startsWith("footer is not found")) { + throw RubyZlib.newNoFooter(context, "footer is not found"); + } else if (m.startsWith("incorrect data check")) { + throw RubyZlib.newCRCError(context, "invalid compressed data -- crc error"); + } else if (m.startsWith("incorrect length check")) { + throw RubyZlib.newLengthError(context, "invalid compressed data -- length error"); + } else { + throw RubyZlib.newDataError(context, ioe.getMessage()); + } + } + } + + @Deprecated + public IRubyObject readpartial(IRubyObject[] args) { + return readpartial(getCurrentContext(), args); + } + + @JRubyMethod(name = "readpartial", required = 1, optional = 1, checkArity = false) + public IRubyObject readpartial(ThreadContext context, IRubyObject[] args) { + int argc = Arity.checkArgumentCount(context, args, 1, 2); + + try { + int len = toInt(context, args[0]); + if (len < 0) throw argumentError(context, "negative length " + len + " given"); + + return argc > 1 && !args[1].isNil() ? + readPartial(context, len, castAsString(context, args[1])) : + readPartial(context, len, null); + } catch (IOException ioe) { + throw context.runtime.newIOErrorFromException(ioe); + } + } + + private IRubyObject readPartial(ThreadContext context, int len, RubyString outbuf) throws IOException { + ByteList val = newReadByteList(10); + byte[] buffer = new byte[len]; + int read = bufferedStream.read(buffer, 0, len); + + if (read == -1) return context.nil; + + val.append(buffer, 0, read); + this.position += val.length(); + + if (outbuf != null) outbuf.view(val); + + return newStr(context, val); + } + + private RubyString readAll(ThreadContext context) throws IOException { + return readAll(context, -1); + } + + private RubyString readAll(ThreadContext context, int limit) throws IOException { + ByteList val = newReadByteList(10); + int rest = limit == -1 ? BUFF_SIZE : limit; + byte[] buffer = new byte[rest]; + + while (rest > 0) { + int read = bufferedStream.read(buffer, 0, rest); + if (read == -1) break; + + val.append(buffer, 0, read); + if (limit != -1) rest -= read; + } + + fixBrokenTrailingCharacter(context, val); + + this.position += val.length(); + return newStr(context, val); + } + + + // FIXME: I think offset == 0 should return empty bytelist and not null + // mri: gzfile_read + // This returns a bucket of bytes trying to read length bytes. + private ByteList readSize(int length) throws IOException { + byte[] buffer = new byte[length]; + int toRead = length; + int offset = 0; + + while (toRead > 0) { + int read = bufferedStream.read(buffer, offset, toRead); + + if (read == -1) { + if (offset == 0) return null; // we're at EOF right away + break; + } + + toRead -= read; + offset += read; + } // hmm... + + this.position += length - toRead; + + return new ByteList(buffer, 0, length - toRead, false); + } + + @Deprecated(since = "10.0") + public IRubyObject set_lineno(IRubyObject lineArg) { + return set_lineno(getCurrentContext(), lineArg); + } + + @JRubyMethod(name = "lineno=") + public IRubyObject set_lineno(ThreadContext context, IRubyObject lineArg) { + line = toInt(context, lineArg); + + return lineArg; + } + + @Deprecated(since = "10.0") + public IRubyObject pos() { + return pos(getCurrentContext()); + } + + @JRubyMethod(name = {"pos", "tell"}) + public IRubyObject pos(ThreadContext context) { + return asFixnum(context, position); + } + + @Deprecated + public IRubyObject readchar() { + return readchar(getCurrentContext()); + } + + @JRubyMethod(name = "readchar") + public IRubyObject readchar(ThreadContext context) { + try { + int value = bufferedStream.read(); + if (value == -1) throw context.runtime.newEOFError(); + + position++; + + return asFixnum(context, value); + } catch (IOException ioe) { + throw context.runtime.newIOErrorFromException(ioe); + } + } + + @Deprecated + public IRubyObject getc() { + return getbyte(getCurrentContext()); + } + + @JRubyMethod(name = "getbyte") + public IRubyObject getbyte(ThreadContext context) { + try { + int value = bufferedStream.read(); + if (value == -1) return context.nil; + + position++; + + return asFixnum(context, value); + } catch (IOException ioe) { + throw context.runtime.newIOErrorFromException(ioe); + } + } + + @Deprecated + public IRubyObject getbyte() { + return getbyte(getCurrentContext()); + } + + @Deprecated + public IRubyObject readbyte() { + return readbyte(getCurrentContext()); + } + + @JRubyMethod(name = "readbyte") + public IRubyObject readbyte(ThreadContext context) { + IRubyObject dst = getbyte(context); + if (dst.isNil()) throw context.runtime.newEOFError(); + return dst; + } + + @JRubyMethod(name = "getc") + public IRubyObject getc(ThreadContext context) { + try { + int value = bufferedStream.read(); + if (value == -1) return context.nil; + + position++; + // TODO: must handle encoding. Move encoding handling methods to util class from RubyIO and use it. + // TODO: StringIO needs a love, too. + return newString(context, String.valueOf((char) (value & 0xFF))); + } catch (IOException ioe) { + throw context.runtime.newIOErrorFromException(ioe); + } + } + + private boolean isEof() throws IOException { + if (bufferedStream.available() == 0) return true; + + // Java's GZIPInputStream behavior is such + // that it says that more bytes available even + // when we are right before the EOF, but not yet + // encountered the actual EOF during the reading. + // So, we compensate for that to provide MRI + // compatible behavior. + byte[] bytes = new byte[16]; + int read = bufferedStream.read(bytes, 0, bytes.length); + + // We are already at EOF. + if (read == -1) return true; + + bufferedStream.unread(bytes, 0, read); + + return bufferedStream.available() == 0; + } + + @Override + @JRubyMethod(name = "close") + public IRubyObject close(ThreadContext context) { + if (!closed) { + try { + /** + * We call internal IO#close directly, not via + * IOInputStream#close. IOInputStream#close directly invoke + * IO.getOutputStream().close() for IO object instead of just + * calling IO#cloase of Ruby. It causes EBADF at + * OpenFile#finalize. + * + * CAUTION: bufferedStream.close() will not cause + * 'IO.getOutputStream().close()', becase 'false' has been given + * as third augument in constructing GZIPInputStream. + * + * TODO: implement this without IOInputStream? Not so hard. + */ + bufferedStream.close(); + if (realIo.respondsTo("close")) realIo.callMethod(context, "close"); + } catch (IOException ioe) { + throw context.runtime.newIOErrorFromException(ioe); + } + } + this.closed = true; + return realIo; + } + + @Deprecated(since = "10.0") + public IRubyObject eof() { + return eof(getCurrentContext()); + } + + @JRubyMethod(name = "eof") + public IRubyObject eof(ThreadContext context) { + try { + return isEof() ? context.tru : context.fals; + } catch (IOException ioe) { + throw context.runtime.newIOErrorFromException(ioe); + } + } + + @Deprecated(since = "10.0") + public IRubyObject eof_p() { + return eof_p(getCurrentContext()); + } + + @JRubyMethod(name = "eof?") + public IRubyObject eof_p(ThreadContext context) { + return eof(context); + } + + @Deprecated(since = "10.0") + public IRubyObject unused() { + return unused(getCurrentContext()); + } + + @JRubyMethod + public IRubyObject unused(ThreadContext context) { + byte[] tmp = io.getAvailIn(); + + return tmp == null ? context.nil : newString(context, new ByteList(tmp)); + } + + @Override + @JRubyMethod + public IRubyObject crc(ThreadContext context) { + long crc = 0; + + try { + crc = io.getCRC(); + } catch (GZIPException e) { + } + + return asFixnum(context, crc); + } + + @Override + @JRubyMethod + public IRubyObject os_code(ThreadContext context) { + int os = io.getOS(); + + if (os == 255) os = (byte) 0x0b; // NTFS filesystem (NT), because CRuby's test_zlib expect it. + + return asFixnum(context, os & 0xff); + } + + @Override + @JRubyMethod + public IRubyObject orig_name(ThreadContext context) { + String name = io.getName(); + + nullFreeOrigName = newString(context, name); + + return super.orig_name(context); + } + + @Override + @JRubyMethod + public IRubyObject comment(ThreadContext context) { + String comment = io.getComment(); + + nullFreeComment = newString(context, comment); + + return super.comment(context); + } + + @JRubyMethod(optional = 1, checkArity = false) + public IRubyObject each(ThreadContext context, IRubyObject[] args, Block block) { + int argc = Arity.checkArgumentCount(context, args, 0, 1); + + if (!block.isGiven()) return RubyEnumerator.enumeratorize(context.runtime, this, "each", args); + + ByteList sep = ((RubyString) globalVariables(context).get("$/")).getByteList(); + + if (argc > 0 && !args[0].isNil()) { + sep = args[0].convertToString().getByteList(); + } + + try { + for (IRubyObject result = internalSepGets(context, sep); !result.isNil(); result = internalSepGets(context, sep)) { + block.yield(context, result); + } + } catch (IOException ioe) { + throw context.runtime.newIOErrorFromException(ioe); + } + + return context.nil; + } + + @JRubyMethod(optional = 1, checkArity = false) + public IRubyObject each_line(ThreadContext context, IRubyObject[] args, Block block) { + if (!block.isGiven()) return RubyEnumerator.enumeratorize(context.runtime, this, "each_line", args); + + return each(context, args, block); + } + + @JRubyMethod + public IRubyObject ungetc(ThreadContext context, IRubyObject cArg) { + if (cArg.isNil()) return context.nil; + + RubyString c = cArg instanceof RubyInteger cint ? + EncodingUtils.encUintChr(context, cint.asInt(context), getReadEncoding(context)) : + cArg.convertToString(); + + try { + byte[] bytes = c.getBytes(); + bufferedStream.unread(bytes); + position -= bytes.length; + } catch (IOException ioe) { + throw context.runtime.newIOErrorFromException(ioe); + } + + return context.nil; + } + + @Deprecated(since = "10.0") + public IRubyObject ungetbyte(IRubyObject b) { + return ungetbyte(getCurrentContext(), b); + } + + @JRubyMethod + public IRubyObject ungetbyte(ThreadContext context, IRubyObject b) { + if (b.isNil()) return b; + + try { + bufferedStream.unread(toInt(context, b)); + position--; + } catch (IOException ioe) { + throw context.runtime.newIOErrorFromException(ioe); + } + + return context.nil; + } + + @JRubyMethod(optional = 1, checkArity = false) + public IRubyObject readlines(ThreadContext context, IRubyObject[] args) { + int argc = Arity.checkArgumentCount(context, args, 0, 1); + + List array = new ArrayList<>(); + + if (argc != 0 && args[0].isNil()) { + array.add(read(context, IRubyObject.NULL_ARRAY)); + } else { + ByteList sep = ((RubyString) globalVariables(context).get("$/")).getByteList(); + + if (argc > 0) sep = args[0].convertToString().getByteList(); + + try { + for (IRubyObject result = internalSepGets(context, sep); !result.isNil(); result = internalSepGets(context, sep)) { + array.add(result); + } + } catch (IOException ioe) { + throw context.runtime.newIOErrorFromException(ioe); + } + } + + return newArray(context, array); + } + + @JRubyMethod + public IRubyObject each_byte(ThreadContext context, Block block) { + if (!block.isGiven()) return RubyEnumerator.enumeratorize(context.runtime, this, "each_byte"); + + try { + int value = bufferedStream.read(); + + while (value != -1) { + position++; + block.yield(context, asFixnum(context, value)); + value = bufferedStream.read(); + } + } catch (IOException ioe) { + throw context.runtime.newIOErrorFromException(ioe); + } + + return context.nil; + } + + @JRubyMethod + public IRubyObject each_char(ThreadContext context, Block block) { + final Ruby runtime = context.runtime; + if (!block.isGiven()) return RubyEnumerator.enumeratorize(runtime, this, "each_char"); + + try { + int value = bufferedStream.read(); + while(value != -1) { + position++; + // TODO: must handle encoding. Move encoding handling methods to util class from RubyIO and use it. + // TODO: StringIO needs a love, too. + block.yield(context, runtime.newString(String.valueOf((char) (value & 0xFF)))); + value = bufferedStream.read(); + } + } catch(IOException ioe) { + throw runtime.newIOErrorFromException(ioe); + } + + return context.nil; + } + + /** + * Document-method: Zlib::GzipReader.zcat + * + * call-seq: + * Zlib::GzipReader.zcat(io, options = {}, &block) => nil + * Zlib::GzipReader.zcat(io, options = {}) => string + * + * Decompresses all gzip data in the +io+, handling multiple gzip + * streams until the end of the +io+. There should not be any non-gzip + * data after the gzip streams. + * + * If a block is given, it is yielded strings of uncompressed data, + * and the method returns +nil+. + * If a block is not given, the method returns the concatenation of + * all uncompressed data in all gzip streams. + */ + @JRubyMethod(required = 1, optional = 1, checkArity = false, meta = true) + public static IRubyObject zcat(ThreadContext context, IRubyObject klass, IRubyObject[] args, Block block) { + Arity.checkArgumentCount(context, args, 1, 2); + + IRubyObject io, unused; + JZlibRubyGzipReader obj; + RubyString buf = null, tmpbuf; + long pos; + + io = args[0]; + + try { + do { + obj = (JZlibRubyGzipReader) ((RubyClass) klass).newInstance(context, args, block); + if (block.isGiven()) { + obj.each(context, IRubyObject.NULL_ARRAY, block); + } + else { + if (buf == null) buf = newEmptyString(context); + tmpbuf = obj.readAll(context); + buf.cat(tmpbuf); + } + + obj.read(context, IRubyObject.NULL_ARRAY); + pos = toLong(context, io.callMethod(context, "pos")); + unused = obj.unused(); + obj.finish(context); + if (!unused.isNil()) { + pos -= toLong(context, unused.callMethod(context, "length")); + io.callMethod(context, "pos=", asFixnum(context, pos)); + } + } while (pos < toLong(context, io.callMethod(context, "size"))); + } catch (IOException ioe) { + throw context.runtime.newIOErrorFromException(ioe); + } + + return block.isGiven() ? context.nil : buf; + } + + private void fixBrokenTrailingCharacter(ThreadContext context, ByteList result) throws IOException { + // fix broken trailing character + int extraBytes = StringSupport.bytesToFixBrokenTrailingCharacter(result.getUnsafeBytes(), result.getBegin(), + result.getRealSize(), getReadEncoding(context), result.length()); + + for (int i = 0; i < extraBytes; i++) { + int read = bufferedStream.read(); + if (read == -1) break; + + result.append(read); + } + } + + private int line = 0; + private long position = 0; + private IOInputStream ioInputStream; + private GZIPInputStream io; + private PushbackInputStream bufferedStream; +} diff --git a/ext/java/org/jruby/ext/zlib/JZlibRubyGzipWriter.java b/ext/java/org/jruby/ext/zlib/JZlibRubyGzipWriter.java new file mode 100644 index 0000000..3945f45 --- /dev/null +++ b/ext/java/org/jruby/ext/zlib/JZlibRubyGzipWriter.java @@ -0,0 +1,432 @@ +/* + **** BEGIN LICENSE BLOCK ***** + * Version: EPL 2.0/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Eclipse Public + * License Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.eclipse.org/legal/epl-v20.html + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing + * rights and limitations under the License. + * + * Alternatively, the contents of this file may be used under the terms of + * either of the GNU General Public License Version 2 or later (the "GPL"), + * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the EPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the EPL, the GPL or the LGPL. + ***** END LICENSE BLOCK *****/ + +package org.jruby.ext.zlib; + +import com.jcraft.jzlib.Deflater; +import com.jcraft.jzlib.GZIPException; +import com.jcraft.jzlib.GZIPOutputStream; +import com.jcraft.jzlib.JZlib; +import org.jcodings.specific.ASCIIEncoding; +import org.jruby.Ruby; +import org.jruby.RubyBasicObject; +import org.jruby.RubyClass; +import org.jruby.RubyIO; +import org.jruby.RubyKernel; +import org.jruby.RubyNumeric; +import org.jruby.RubyString; +import org.jruby.RubyTime; +import org.jruby.anno.JRubyClass; +import org.jruby.anno.JRubyMethod; +import org.jruby.runtime.Arity; +import org.jruby.runtime.Block; +import org.jruby.runtime.Helpers; +import org.jruby.runtime.ThreadContext; +import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.util.ByteList; +import org.jruby.util.IOOutputStream; +import org.jruby.util.TypeConverter; +import org.jruby.util.io.EncodingUtils; + +import java.io.IOException; + +import static org.jruby.api.Access.fileClass; +import static org.jruby.api.Access.globalVariables; +import static org.jruby.api.Convert.asFixnum; +import static org.jruby.api.Convert.toByte; +import static org.jruby.api.Convert.toInt; +import static org.jruby.api.Convert.toLong; +import static org.jruby.api.Create.dupString; +import static org.jruby.api.Create.newString; +import static org.jruby.runtime.Visibility.PRIVATE; + +@JRubyClass(name = "Zlib::GzipWriter", parent = "Zlib::GzipFile") +public class JZlibRubyGzipWriter extends RubyGzipFile { + @JRubyMethod(name = "new", rest = true, meta = true, keywords = true) + public static IRubyObject newInstance(ThreadContext context, IRubyObject recv, IRubyObject[] args, Block block) { + JZlibRubyGzipWriter result = newInstance(context, (RubyClass) recv, args); + + return RubyGzipFile.wrapBlock(context, result, block); + } + + @Deprecated(since = "10.0") + public static JZlibRubyGzipWriter newInstance(IRubyObject recv, IRubyObject[] args) { + return newInstance(((RubyBasicObject) recv).getCurrentContext(), (RubyClass) recv, args); + } + + public static JZlibRubyGzipWriter newInstance(ThreadContext context, RubyClass klass, IRubyObject[] args) { + JZlibRubyGzipWriter result = (JZlibRubyGzipWriter) klass.allocate(context); + + result.callInit(args, Block.NULL_BLOCK); + + return result; + } + + @JRubyMethod(name = "open", required = 1, optional = 3, checkArity = false, meta = true) + public static IRubyObject open(ThreadContext context, IRubyObject recv, IRubyObject[] args, Block block) { + Arity.checkArgumentCount(context, args, 1, 4); + args[0] = Helpers.invoke(context, fileClass(context), "open", args[0], newString(context, "wb")); + + JZlibRubyGzipWriter gzio = newInstance(context, (RubyClass) recv, args); + + return RubyGzipFile.wrapBlock(context, gzio, block); + } + + public JZlibRubyGzipWriter(Ruby runtime, RubyClass type) { + super(runtime, type); + } + + @Deprecated(since = "10.0") + public IRubyObject initialize(IRubyObject[] args) { + return initialize(getCurrentContext(), args, Block.NULL_BLOCK); + } + + @JRubyMethod(name = "initialize", rest = true, visibility = PRIVATE) + public IRubyObject initialize(ThreadContext context, IRubyObject[] args, Block block) { + IRubyObject opt = context.nil; + + int argc = args.length; + if (argc > 1) { + opt = TypeConverter.checkHashType(context.runtime, opt); + if (!opt.isNil()) argc--; + } + + level = processLevel(context, argc, args); + + // unused; could not figure out how to get JZlib to take this right + /*int strategy = */processStrategy(context, argc, args); + + initializeCommon(context, args[0], level); + + ecopts(context, opt); + + return this; + } + + private int processStrategy(ThreadContext context, int argc, IRubyObject[] args) { + return argc < 3 ? JZlib.Z_DEFAULT_STRATEGY : RubyZlib.FIXNUMARG(context, args[2], JZlib.Z_DEFAULT_STRATEGY); + } + + private int processLevel(ThreadContext context, int argc, IRubyObject[] args) { + int level = argc < 2 ? JZlib.Z_DEFAULT_COMPRESSION : RubyZlib.FIXNUMARG(context, args[1], JZlib.Z_DEFAULT_COMPRESSION); + + checkLevel(context, level); + + return level; + } + + private IRubyObject initializeCommon(ThreadContext context, IRubyObject stream, int level) { + Ruby runtime = context.runtime; + realIo = stream; + try { + // the 15+16 here is copied from a Deflater default constructor + Deflater deflater = new Deflater(level, 15+16, false); + final IOOutputStream ioOutputStream = new IOOutputStream(realIo, false, false) { + /** + * Customize IOOutputStream#write(byte[], int, int) to create a defensive copy of the byte array + * that GZIPOutputStream hands us. + * + * That byte array is a reference to one of GZIPOutputStream's internal byte buffers. + * The base IOOutputStream#write(byte[], int, int) uses the bytes it is handed to back a + * copy-on-write ByteList. So, without this defensive copy, those two classes overwrite each + * other's bytes, corrupting our output. + */ + @Override + public void write(byte[] bytes, int off, int len) throws IOException { + byte[] bytesCopy = new byte[len]; + System.arraycopy(bytes, off, bytesCopy, 0, len); + super.write(bytesCopy, 0, len); + } + }; + + io = new GZIPOutputStream(ioOutputStream, deflater, 512, false); + + // set mtime to current time in case it is never updated + long now = System.currentTimeMillis(); + this.mtime = RubyTime.newTime(runtime, now); + io.setModifiedTime(now / 1000); + + return this; + } catch (IOException ioe) { + throw runtime.newIOErrorFromException(ioe); + } + } + + private static void checkLevel(ThreadContext context, int level) { + if (level != JZlib.Z_DEFAULT_COMPRESSION && (level < JZlib.Z_NO_COMPRESSION || level > JZlib.Z_BEST_COMPRESSION)) { + throw RubyZlib.newStreamError(context, "stream error: invalid level"); + } + } + + + @Override + @JRubyMethod(name = "close") + public IRubyObject close(ThreadContext context) { + if (!closed) { + try { + io.close(); + if (realIo.respondsTo("close")) realIo.callMethod(context, "close"); + } catch (IOException ioe) { + throw context.runtime.newIOErrorFromException(ioe); + } + } + + this.closed = true; + + return realIo; + } + + @JRubyMethod(name = {"append", "<<"}) + public IRubyObject append(IRubyObject p1) { + this.write(p1); + + return this; + } + + @JRubyMethod(name = "printf", required = 1, rest = true, checkArity = false) + public IRubyObject printf(ThreadContext context, IRubyObject[] args) { + write(RubyKernel.sprintf(context, this, args)); + + return context.nil; + } + + /** + * @param args + * @return + * @deprecated Use {@link JZlibRubyGzipWriter#print(ThreadContext, IRubyObject[])} instead. + */ + @Deprecated(since = "10.0") + public IRubyObject print(IRubyObject[] args) { + return print(getCurrentContext(), args); + } + + @JRubyMethod(name = "print", rest = true) + public IRubyObject print(ThreadContext context, IRubyObject[] args) { + if (args.length != 0) { + for (int i = 0, j = args.length; i < j; i++) { + write(args[i]); + } + } + + IRubyObject sep = globalVariables(context).get("$\\"); + if (!sep.isNil()) write(sep); + + return context.nil; + } + + @Deprecated(since = "10.0") + public IRubyObject pos() { + return pos(getCurrentContext()); + } + + @JRubyMethod(name = {"pos", "tell"}) + public IRubyObject pos(ThreadContext context) { + return asFixnum(context, io.getTotalIn()); + } + + @Deprecated(since = "10.0") + public IRubyObject set_orig_name(IRubyObject obj) { + return set_orig_name(getCurrentContext(), obj); + } + + @JRubyMethod(name = "orig_name=") + public IRubyObject set_orig_name(ThreadContext context, IRubyObject obj) { + nullFreeOrigName = ensureNonNull(dupString(context, obj.convertToString())); + + try { + io.setName(nullFreeOrigName.toString()); + } catch (GZIPException e) { + throw RubyZlib.newGzipFileError(context, "header is already written"); + } + + return obj; + } + + @Deprecated(since = "10.0") + public IRubyObject set_comment(IRubyObject obj) { + return set_comment(getCurrentContext(), obj); + } + + @JRubyMethod(name = "comment=") + public IRubyObject set_comment(ThreadContext context, IRubyObject obj) { + nullFreeComment = ensureNonNull(dupString(context, obj.convertToString())); + + try { + io.setComment(nullFreeComment.toString()); + } catch (GZIPException e) { + throw RubyZlib.newGzipFileError(context, "header is already written"); + } + + return obj; + } + + private RubyString ensureNonNull(RubyString obj) { + String str = obj.toString(); + + if (str.indexOf('\0') >= 0) { + String trim = str.substring(0, str.indexOf('\0')); + obj.setValue(new ByteList(trim.getBytes())); + } + + return obj; + } + + @Deprecated(since = "10.0") + public IRubyObject putc(IRubyObject p1) { + return putc(getCurrentContext(), p1); + } + + @JRubyMethod(name = "putc") + public IRubyObject putc(ThreadContext context, IRubyObject p1) { + try { + io.write(toByte(context, p1)); + + return p1; + } catch (IOException ioe) { + throw context.runtime.newIOErrorFromException(ioe); + } + } + + @JRubyMethod(name = "puts", rest = true) + public IRubyObject puts(ThreadContext context, IRubyObject[] args) { + return RubyIO.puts(context, this, args); + } + + @Override + public IRubyObject finish(ThreadContext context) { + if (!finished) { + try { + io.finish(); + } catch (IOException ioe) { + throw context.runtime.newIOErrorFromException(ioe); + } + } + + finished = true; + + return realIo; + } + + @Deprecated + public IRubyObject flush(IRubyObject[] args) { + return flush(getCurrentContext(), args); + } + + @JRubyMethod(name = "flush", optional = 1, checkArity = false) + public IRubyObject flush(ThreadContext context, IRubyObject[] args) { + int argc = Arity.checkArgumentCount(context, args, 0, 1); + int flush = argc > 0 && !args[0].isNil() ? toInt(context, args[0]) : JZlib.Z_SYNC_FLUSH; + boolean tmp = io.getSyncFlush(); + + try { + if (flush != 0 /* + * NO_FLUSH + */) { + io.setSyncFlush(true); + } + io.flush(); + } catch (IOException ioe) { + throw context.runtime.newIOErrorFromException(ioe); + } finally { + io.setSyncFlush(tmp); + } + + return context.nil; + } + + @Deprecated(since = "10.0") + public IRubyObject set_mtime(IRubyObject arg) { + return set_mtime(getCurrentContext(), arg); + } + + @JRubyMethod(name = "mtime=") + public IRubyObject set_mtime(ThreadContext context, IRubyObject arg) { + if (arg instanceof RubyTime timeArg) { + this.mtime = timeArg; + } else if (!arg.isNil()) { + this.mtime = RubyTime.newTime(context.runtime, toLong(context, arg) * 1000); + } + try { + io.setModifiedTime(mtime.to_i_long()); + } catch (GZIPException e) { + throw RubyZlib.newGzipFileError(context, "header is already written"); + } + + return context.nil; + } + + @Override + @JRubyMethod(name = "crc") + public IRubyObject crc(ThreadContext context) { + long crc = 0L; + + try { + crc = io.getCRC(); + } catch (GZIPException e) { + // not calculated yet + } + + return asFixnum(context, crc); + } + + @Deprecated + public IRubyObject write(IRubyObject p1) { + return write(getCurrentContext(), p1); + } + + @JRubyMethod(name = "write") + public IRubyObject write(ThreadContext context, IRubyObject p1) { + RubyString str = p1.asString(); + + if (enc2 != null && enc2 != ASCIIEncoding.INSTANCE) { + str = EncodingUtils.strConvEncOpts(context, str, str.getEncoding(), enc2, 0, context.nil); + } + + try { + // TODO: jzlib-1.1.0.jar throws IndexOutOfBoundException for zero length buffer. + if (!str.isEmpty()) { + io.write(str.getByteList().getUnsafeBytes(), str.getByteList().begin(), str.getByteList().length()); + } + + return asFixnum(context, str.getByteList().length()); + } catch (IOException ioe) { + throw context.runtime.newIOErrorFromException(ioe); + } + } + + @Override + @JRubyMethod + public IRubyObject set_sync(ThreadContext context, IRubyObject arg) { + IRubyObject s = super.set_sync(context, arg); + + io.setSyncFlush(sync); + + return s; + } + + private GZIPOutputStream io; +} diff --git a/ext/java/org/jruby/ext/zlib/RubyGzipFile.java b/ext/java/org/jruby/ext/zlib/RubyGzipFile.java new file mode 100644 index 0000000..34d8896 --- /dev/null +++ b/ext/java/org/jruby/ext/zlib/RubyGzipFile.java @@ -0,0 +1,386 @@ +/* + **** BEGIN LICENSE BLOCK ***** + * Version: EPL 2.0/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Eclipse Public + * License Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.eclipse.org/legal/epl-v20.html + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing + * rights and limitations under the License. + * + * Alternatively, the contents of this file may be used under the terms of + * either of the GNU General Public License Version 2 or later (the "GPL"), + * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the EPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the EPL, the GPL or the LGPL. + ***** END LICENSE BLOCK *****/ + +package org.jruby.ext.zlib; + +import org.jcodings.Encoding; +import org.jcodings.transcode.EConv; +import org.joda.time.DateTime; +import org.jruby.Ruby; +import org.jruby.RubyBasicObject; +import org.jruby.RubyClass; +import org.jruby.RubyModule; +import org.jruby.RubyObject; +import org.jruby.RubyString; +import org.jruby.RubyTime; +import org.jruby.anno.JRubyClass; +import org.jruby.anno.JRubyMethod; +import org.jruby.api.Access; +import org.jruby.runtime.Block; +import org.jruby.runtime.Helpers; +import org.jruby.runtime.JavaSites; +import org.jruby.runtime.ThreadContext; +import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.util.ByteList; +import org.jruby.util.io.EncodingUtils; +import org.jruby.util.io.IOEncodable; + +import static org.jruby.api.Convert.asFixnum; +import static org.jruby.api.Create.newString; + +@JRubyClass(name = "Zlib::GzipFile") +public class RubyGzipFile extends RubyObject implements IOEncodable { + @JRubyClass(name = "Zlib::GzipFile::Error", parent = "Zlib::Error") + public static class Error {} + + @JRubyClass(name = "Zlib::GzipFile::CRCError", parent = "Zlib::GzipFile::Error") + public static class CRCError extends Error {} + + @JRubyClass(name = "Zlib::GzipFile::NoFooter", parent = "Zlib::GzipFile::Error") + public static class NoFooter extends Error {} + + @JRubyClass(name = "Zlib::GzipFile::LengthError", parent = "Zlib::GzipFile::Error") + public static class LengthError extends Error {} + + static IRubyObject wrapBlock(ThreadContext context, RubyGzipFile instance, Block block) { + if (block.isGiven()) { + try { + return block.yield(context, instance); + } finally { + if (!instance.isClosed()) { + instance.close(); + } + } + } + return instance; + } + + @Deprecated(since = "10.0") + public static IRubyObject wrap(ThreadContext context, IRubyObject recv, IRubyObject io, Block block) { + return wrap(context, recv, new IRubyObject[]{io}, block); + } + + @Deprecated(since = "10.0") + public static IRubyObject wrap19(ThreadContext context, IRubyObject recv, IRubyObject[] args, Block block) { + return wrap(context, recv, args, block); + } + + @JRubyMethod(meta = true, name = "wrap", required = 1, optional = 1, checkArity = false) + public static IRubyObject wrap(ThreadContext context, IRubyObject recv, IRubyObject[] args, Block block) { + RubyGzipFile instance = ((RubyModule) recv).isKindOfModule(Access.getClass(context, "Zlib", "GzipWriter")) ? + JZlibRubyGzipWriter.newInstance(context, (RubyClass) recv, args) : + JZlibRubyGzipReader.newInstance(context, (RubyClass) recv, args); + + return wrapBlock(context, instance, block); + } + + @Deprecated(since = "10.0") + public static RubyGzipFile newInstance(IRubyObject recv, Block block) { + return newInstance(((RubyBasicObject) recv).getCurrentContext(), recv, block); + } + + @JRubyMethod(name = "new", meta = true) + public static RubyGzipFile newInstance(ThreadContext context, IRubyObject recv, Block block) { + RubyClass klass = (RubyClass) recv; + + RubyGzipFile result = (RubyGzipFile) klass.allocate(context); + + result.callInit(IRubyObject.NULL_ARRAY, block); + + return result; + } + + // These methods are here to avoid defining a singleton #path on every instance, as in MRI + + @JRubyMethod + public IRubyObject path(ThreadContext context) { + return this.realIo.callMethod(context, "path"); + } + + @JRubyMethod(name = "respond_to?", frame = true) + public IRubyObject respond_to(ThreadContext context, IRubyObject name) { + if (name.asJavaString().equals("path")) { + return sites(context).reader_respond_to.call(context, this, this.realIo, name); + } + + return Helpers.invokeSuper(context, this, name, Block.NULL_BLOCK); + } + + @JRubyMethod(name = "respond_to?", frame = true) + public IRubyObject respond_to(ThreadContext context, IRubyObject name, IRubyObject includePrivate) { + if (name.asJavaString().equals("path")) { + return sites(context).reader_respond_to.call(context, this, this.realIo, name, includePrivate); + } + + return Helpers.invokeSuper(context, this, name, Block.NULL_BLOCK); + } + + public RubyGzipFile(Ruby runtime, RubyClass type) { + super(runtime, type); + mtime = RubyTime.newTime(runtime, new DateTime()); + enc = null; + enc2 = null; + } + + // rb_gzfile_ecopts + protected void ecopts(ThreadContext context, IRubyObject opts) { + if (!opts.isNil()) { + EncodingUtils.ioExtractEncodingOption(context, this, opts, null); + } + if (enc2 != null) { + IRubyObject[] outOpts = new IRubyObject[]{opts}; + ecflags = EncodingUtils.econvPrepareOpts(context, opts, outOpts); + ec = EncodingUtils.econvOpenOpts(context, enc.getName(), enc2.getName(), ecflags, opts); + ecopts = opts; + } + } + + @Deprecated(since = "10.0") + public Encoding getReadEncoding() { + return getReadEncoding(getCurrentContext()); + } + + public Encoding getReadEncoding(ThreadContext context) { + return enc == null ? context.runtime.getDefaultExternalEncoding() : enc; + } + + public Encoding getEnc() { + return enc; + } + + public Encoding getInternalEncoding() { + return enc2 == null ? getEnc() : enc2; + } + + public Encoding getEnc2() { + return enc2; + } + + @Deprecated(since = "10.0") + protected RubyString newStr(Ruby runtime, ByteList value) { + return newStr(runtime.getCurrentContext(), value); + } + + // c: gzfile_newstr + protected RubyString newStr(ThreadContext context, ByteList value) { + if (enc2 == null) return newString(context, value, getReadEncoding(context)); + + return ec != null && enc2.isDummy() ? + newString(context, EncodingUtils.econvStrConvert(context, ec, value, 0), getEnc()) : + EncodingUtils.strConvEncOpts(context, newString(context, value), enc2, enc, ecflags, ecopts); + } + + @Deprecated + public IRubyObject os_code() { + return os_code(getCurrentContext()); + } + + @JRubyMethod(name = "os_code") + public IRubyObject os_code(ThreadContext context) { + return asFixnum(context, osCode & 0xff); + } + + @Deprecated + public IRubyObject closed_p() { + return closed_p(getCurrentContext()); + } + + @JRubyMethod(name = "closed?") + public IRubyObject closed_p(ThreadContext context) { + return closed ? context.tru : context.fals; + } + + protected boolean isClosed() { + return closed; + } + + @Deprecated(since = "10.0") + public IRubyObject orig_name() { + return orig_name(getCurrentContext()); + } + + @JRubyMethod(name = "orig_name") + public IRubyObject orig_name(ThreadContext context) { + if (closed) throw RubyZlib.newGzipFileError(context, "closed gzip stream"); + + return nullFreeOrigName == null ? context.nil : nullFreeOrigName; + } + + @Deprecated(since = "10.0") + public IRubyObject to_io() { + return to_io(getCurrentContext()); + } + + @JRubyMethod(name = "to_io") + public IRubyObject to_io(ThreadContext context) { + return realIo; + } + + @Deprecated(since = "10.0") + public IRubyObject comment() { + return comment(getCurrentContext()); + } + + @JRubyMethod(name = "comment") + public IRubyObject comment(ThreadContext context) { + if (closed) throw RubyZlib.newGzipFileError(context, "closed gzip stream"); + + return nullFreeComment == null ? context.nil : nullFreeComment; + } + + @Deprecated + public IRubyObject crc() { + return crc(getCurrentContext()); + } + + @JRubyMethod(name = "crc") + public IRubyObject crc(ThreadContext context) { + return asFixnum(context, 0); + } + + @JRubyMethod(name = "mtime") + public IRubyObject mtime() { + return mtime; + } + + @Deprecated + public IRubyObject sync() { + return sync(getCurrentContext()); + } + + @JRubyMethod(name = "sync") + public IRubyObject sync(ThreadContext context) { + return sync ? context.tru : context.fals; + } + + @Deprecated(since = "10.0") + public IRubyObject finish() { + return finish(getCurrentContext()); + } + + @JRubyMethod(name = "finish") + public IRubyObject finish(ThreadContext context) { + if (!finished) { + //io.finish(); + } + finished = true; + return realIo; + } + + @Deprecated(since = "10.0") + public IRubyObject close() { + return close(getCurrentContext()); + } + + @JRubyMethod(name = "close") + public IRubyObject close(ThreadContext context) { + return realIo; + } + + @Deprecated + public IRubyObject level() { + return level(getCurrentContext()); + } + + @JRubyMethod(name = "level") + public IRubyObject level(ThreadContext context) { + return asFixnum(context, level); + } + + @Deprecated(since = "10.0") + public IRubyObject set_sync(IRubyObject arg) { + return set_sync(getCurrentContext(), arg); + } + + @JRubyMethod(name = "sync=") + public IRubyObject set_sync(ThreadContext context, IRubyObject arg) { + sync = arg.isTrue(); + return sync ? context.tru : context.fals; + } + + @Override + public void setEnc(Encoding readEncoding) { + this.enc = readEncoding; + } + + @Override + public void setEnc2(Encoding writeEncoding) { + this.enc2 = writeEncoding; + } + + @Override + public void setEcflags(int ecflags) { + this.ecflags = ecflags; + } + + @Override + public int getEcflags() { + return ecflags; + } + + @Override + public void setEcopts(IRubyObject ecopts) { + this.ecopts = ecopts; + } + + @Override + public IRubyObject getEcopts() { + return ecopts; + } + + @Override + public void setBOM(boolean bom) { + this.hasBOM = bom; + } + + @Override + public boolean getBOM() { + return hasBOM; + } + + private static JavaSites.ZlibSites sites(ThreadContext context) { + return context.sites.Zlib; + } + + protected boolean closed = false; + protected boolean finished = false; + protected boolean hasBOM; + protected final byte osCode = Zlib.OS_UNKNOWN; + protected int level = -1; + protected RubyString nullFreeOrigName; + protected RubyString nullFreeComment; + protected IRubyObject realIo; + protected RubyTime mtime; + protected Encoding enc; + protected Encoding enc2; + protected int ecflags; + protected IRubyObject ecopts; + protected EConv ec; + protected boolean sync = false; + protected EConv readTranscoder = null; + protected EConv writeTranscoder = null; +} diff --git a/ext/java/org/jruby/ext/zlib/RubyZlib.java b/ext/java/org/jruby/ext/zlib/RubyZlib.java new file mode 100644 index 0000000..8f287c7 --- /dev/null +++ b/ext/java/org/jruby/ext/zlib/RubyZlib.java @@ -0,0 +1,355 @@ +/***** BEGIN LICENSE BLOCK ***** + * Version: EPL 2.0/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Eclipse Public + * License Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.eclipse.org/legal/epl-v20.html + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing + * rights and limitations under the License. + * + * Copyright (C) 2006 Ola Bini + * Copyright (C) 2006 Ola Bini + * Copyright (C) 2006 Dave Brosius + * Copyright (C) 2006 Peter K Chan + * Copyright (C) 2009 Aurelian Oancea + * Copyright (C) 2009 Vladimir Sizikov + * + * Alternatively, the contents of this file may be used under the terms of + * either of the GNU General Public License Version 2 or later (the "GPL"), + * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the EPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the EPL, the GPL or the LGPL. + ***** END LICENSE BLOCK *****/ + +package org.jruby.ext.zlib; + +import java.util.zip.CRC32; +import java.util.zip.Adler32; + +import org.jruby.RubyBasicObject; +import org.jruby.RubyClass; +import org.jruby.RubyModule; +import org.jruby.RubyException; + +import org.jruby.anno.JRubyClass; +import org.jruby.anno.JRubyMethod; +import org.jruby.anno.JRubyModule; + +import org.jruby.api.Access; +import org.jruby.api.Create; +import org.jruby.exceptions.RaiseException; + +import org.jruby.runtime.Arity; +import org.jruby.runtime.ThreadContext; + +import static org.jruby.api.Access.enumerableModule; +import static org.jruby.api.Access.kernelModule; +import static org.jruby.api.Access.objectClass; +import static org.jruby.api.Access.standardErrorClass; +import static org.jruby.api.Convert.asFixnum; +import static org.jruby.api.Convert.toInt; +import static org.jruby.api.Convert.toLong; +import static org.jruby.api.Create.newString; +import static org.jruby.api.Define.defineModule; +import static org.jruby.runtime.ObjectAllocator.NOT_ALLOCATABLE_ALLOCATOR; +import static org.jruby.runtime.Visibility.*; +import org.jruby.runtime.builtin.IRubyObject; + +import static org.jruby.ext.zlib.Zlib.*; + +import org.jruby.util.ByteList; + +import com.jcraft.jzlib.JZlib; + +@JRubyModule(name="Zlib") +public class RubyZlib { + // version + public final static String ZLIB_VERSION = "1.2.3.3"; + public final static String VERSION = "0.6.0"; + + /** Create the Zlib module and add it to the Ruby runtime. + * + */ + public static RubyModule createZlibModule(ThreadContext context) { + var Object = objectClass(context); + var StandardError = standardErrorClass(context); + var Zlib = defineModule(context, "Zlib"). + defineMethods(context, RubyZlib.class); + var ZlibError = Zlib.defineClassUnder(context, "Error", StandardError, StandardError.getAllocator()); + var errorAllocator = ZlibError.getAllocator(); + Zlib.defineClassUnder(context, "StreamEnd", ZlibError, errorAllocator); + Zlib.defineClassUnder(context, "StreamError", ZlibError, errorAllocator); + Zlib.defineClassUnder(context, "BufError", ZlibError, errorAllocator); + Zlib.defineClassUnder(context, "NeedDict", ZlibError, errorAllocator); + Zlib.defineClassUnder(context, "MemError", ZlibError, errorAllocator); + Zlib.defineClassUnder(context, "VersionError", ZlibError, errorAllocator); + Zlib.defineClassUnder(context, "DataError", ZlibError, errorAllocator); + + RubyClass GzipFile = Zlib.defineClassUnder(context, "GzipFile", Object, RubyGzipFile::new). + defineMethods(context, RubyGzipFile.class); + + GzipFile.defineClassUnder(context, "Error", ZlibError, errorAllocator); + var GZipFileError = GzipFile.defineClassUnder(context, "Error", ZlibError, errorAllocator); + var fileErrorAllocator = ZlibError.getAllocator(); + GZipFileError.addReadAttribute(context, "input"); + GzipFile.defineClassUnder(context, "CRCError", GZipFileError, fileErrorAllocator); + GzipFile.defineClassUnder(context, "NoFooter", GZipFileError, fileErrorAllocator); + GzipFile.defineClassUnder(context, "LengthError", GZipFileError, fileErrorAllocator); + + Zlib.defineClassUnder(context, "GzipReader", GzipFile, JZlibRubyGzipReader::new). + include(context, enumerableModule(context)). + defineMethods(context, JZlibRubyGzipReader.class); + + Zlib.defineClassUnder(context, "GzipWriter", GzipFile, JZlibRubyGzipWriter::new). + defineMethods(context, JZlibRubyGzipWriter.class); + + Zlib.defineConstant(context, "ZLIB_VERSION", newString(context, ZLIB_VERSION)). + defineConstant(context, "VERSION", newString(context, VERSION)). + + defineConstant(context, "BINARY", asFixnum(context, Z_BINARY)). + defineConstant(context, "ASCII", asFixnum(context, Z_ASCII)). + defineConstant(context, "UNKNOWN", asFixnum(context, Z_UNKNOWN)). + + defineConstant(context, "DEF_MEM_LEVEL", asFixnum(context, 8)). + defineConstant(context, "MAX_MEM_LEVEL", asFixnum(context, 9)). + + defineConstant(context, "OS_UNIX", asFixnum(context, OS_UNIX)). + defineConstant(context, "OS_UNKNOWN", asFixnum(context, OS_UNKNOWN)). + defineConstant(context, "OS_CODE", asFixnum(context, OS_CODE)). + defineConstant(context, "OS_ZSYSTEM", asFixnum(context, OS_ZSYSTEM)). + defineConstant(context, "OS_VMCMS", asFixnum(context, OS_VMCMS)). + defineConstant(context, "OS_VMS", asFixnum(context, OS_VMS)). + defineConstant(context, "OS_RISCOS", asFixnum(context, OS_RISCOS)). + defineConstant(context, "OS_MACOS", asFixnum(context, OS_MACOS)). + defineConstant(context, "OS_OS2", asFixnum(context, OS_OS2)). + defineConstant(context, "OS_AMIGA", asFixnum(context, OS_AMIGA)). + defineConstant(context, "OS_QDOS", asFixnum(context, OS_QDOS)). + defineConstant(context, "OS_WIN32", asFixnum(context, OS_WIN32)). + defineConstant(context, "OS_ATARI", asFixnum(context, OS_ATARI)). + defineConstant(context, "OS_MSDOS", asFixnum(context, OS_MSDOS)). + defineConstant(context, "OS_CPM", asFixnum(context, OS_CPM)). + defineConstant(context, "OS_TOPS20", asFixnum(context, OS_TOPS20)). + + defineConstant(context, "DEFAULT_STRATEGY", asFixnum(context, JZlib.Z_DEFAULT_STRATEGY)). + defineConstant(context, "FILTERED", asFixnum(context, JZlib.Z_FILTERED)). + defineConstant(context, "HUFFMAN_ONLY", asFixnum(context, JZlib.Z_HUFFMAN_ONLY)). + + defineConstant(context, "NO_FLUSH", asFixnum(context, JZlib.Z_NO_FLUSH)). + defineConstant(context, "SYNC_FLUSH", asFixnum(context, JZlib.Z_SYNC_FLUSH)). + defineConstant(context, "FULL_FLUSH", asFixnum(context, JZlib.Z_FULL_FLUSH)). + defineConstant(context, "FINISH", asFixnum(context, JZlib.Z_FINISH)). + + defineConstant(context, "NO_COMPRESSION", asFixnum(context, JZlib.Z_NO_COMPRESSION)). + defineConstant(context, "BEST_SPEED", asFixnum(context, JZlib.Z_BEST_SPEED)). + defineConstant(context, "DEFAULT_COMPRESSION", asFixnum(context, JZlib.Z_DEFAULT_COMPRESSION)). + defineConstant(context, "BEST_COMPRESSION", asFixnum(context, JZlib.Z_BEST_COMPRESSION)). + + defineConstant(context, "MAX_WBITS", asFixnum(context, JZlib.MAX_WBITS)); + + // ZStream actually *isn't* allocatable + RubyClass ZStream = Zlib.defineClassUnder(context, "ZStream", Object, NOT_ALLOCATABLE_ALLOCATOR). + defineMethods(context, ZStream.class). + undefMethods(context, "new"); + Zlib.defineClassUnder(context, "Inflate", ZStream, JZlibInflate::new). + defineMethods(context, JZlibInflate.class); + Zlib.defineClassUnder(context, "Deflate", ZStream, JZlibDeflate::new). + defineMethods(context, JZlibDeflate.class); + + kernelModule(context).callMethod(context, "require", newString(context, "stringio")); + + return Zlib; + } + + @JRubyClass(name="Zlib::Error", parent="StandardError") + public static class Error {} + @JRubyClass(name="Zlib::StreamEnd", parent="Zlib::Error") + public static class StreamEnd extends Error {} + @JRubyClass(name="Zlib::StreamError", parent="Zlib::Error") + public static class StreamError extends Error {} + @JRubyClass(name="Zlib::BufError", parent="Zlib::Error") + public static class BufError extends Error {} + @JRubyClass(name="Zlib::NeedDict", parent="Zlib::Error") + public static class NeedDict extends Error {} + @JRubyClass(name="Zlib::MemError", parent="Zlib::Error") + public static class MemError extends Error {} + @JRubyClass(name="Zlib::VersionError", parent="Zlib::Error") + public static class VersionError extends Error {} + @JRubyClass(name="Zlib::DataError", parent="Zlib::Error") + public static class DataError extends Error {} + + @Deprecated(since = "10.0") + public static IRubyObject zlib_version(IRubyObject recv) { + return zlib_version(((RubyBasicObject) recv).getCurrentContext(), recv); + } + + @JRubyMethod(name = "zlib_version", module = true, visibility = PRIVATE) + public static IRubyObject zlib_version(ThreadContext context, IRubyObject recv) { + return ((RubyModule) recv).getConstant(context, "ZLIB_VERSION"); + } + + @JRubyMethod(name = "crc32", optional = 2, checkArity = false, module = true, visibility = PRIVATE) + public static IRubyObject crc32(ThreadContext context, IRubyObject recv, IRubyObject[] args) { + args = Arity.scanArgs(context, args, 0, 2); + ByteList bytes = !args[0].isNil() ? args[0].convertToString().getByteList() : null; + long start = !args[1].isNil() ? toLong(context, args[1]) : 0; + start &= 0xFFFFFFFFL; + + final boolean slowPath = start != 0; + final int bytesLength = bytes == null ? 0 : bytes.length(); + long result = 0; + if (bytes != null) { + CRC32 checksum = new CRC32(); + checksum.update(bytes.getUnsafeBytes(), bytes.begin(), bytesLength); + result = checksum.getValue(); + } + if (slowPath) { + result = JZlib.crc32_combine(start, result, bytesLength); + } + return asFixnum(context, result); + } + + @Deprecated + public static IRubyObject crc32(IRubyObject recv, IRubyObject[] args) { + return crc32(((RubyBasicObject) recv).getCurrentContext(), recv, args); + } + + @JRubyMethod(name = "adler32", optional = 2, checkArity = false, module = true, visibility = PRIVATE) + public static IRubyObject adler32(ThreadContext context, IRubyObject recv, IRubyObject[] args) { + args = Arity.scanArgs(context, args, 0, 2); + ByteList bytes = !args[0].isNil() ? args[0].convertToString().getByteList() : null; + int start = !args[1].isNil() ? (int) toLong(context, args[1]) : 1; + + Adler32 checksum = new Adler32(); + if (bytes != null) checksum.update(bytes.getUnsafeBytes(), bytes.begin(), bytes.length()); + + long result = checksum.getValue(); + if (start != 1) result = JZlib.adler32_combine(start, result, bytes.length()); + + return asFixnum(context, result); + } + + @Deprecated + public static IRubyObject adler32(IRubyObject recv, IRubyObject[] args) { + return adler32(((RubyBasicObject) recv).getCurrentContext(), recv, args); + } + + @JRubyMethod(module = true) + public static IRubyObject inflate(ThreadContext context, IRubyObject recv, IRubyObject string) { + return JZlibInflate.s_inflate(context, recv, string); + } + + @Deprecated(since = "10.0") + public static IRubyObject deflate(IRubyObject recv, IRubyObject[] args) { + return deflate(((RubyBasicObject) recv).getCurrentContext(), recv, args); + } + + @JRubyMethod(required = 1, optional = 1, checkArity = false, module = true) + public static IRubyObject deflate(ThreadContext context, IRubyObject recv, IRubyObject[] args) { + return JZlibDeflate.s_deflate(context, recv, args); + } + + @JRubyMethod(name = "crc_table", module = true, visibility = PRIVATE) + public static IRubyObject crc_table(ThreadContext context, IRubyObject recv) { + int[] table = com.jcraft.jzlib.CRC32.getCRC32Table(); + var result = Create.allocArray(context, table.length); + for (int j: table) result.append(context, asFixnum(context, j & 0xffffffffL)); + return result; + } + + @Deprecated + public static IRubyObject crc_table(IRubyObject recv) { + return crc_table(((RubyBasicObject) recv).getCurrentContext(), recv); + } + + @JRubyMethod(name = "crc32_combine", module = true, visibility = PRIVATE) + public static IRubyObject crc32_combine(ThreadContext context, IRubyObject recv, + IRubyObject arg0, IRubyObject arg1, IRubyObject arg2) { + long crc1 = toLong(context, arg0); + long crc2 = toLong(context, arg1); + long len2 = toLong(context, arg2); + + return asFixnum(context, com.jcraft.jzlib.JZlib.crc32_combine(crc1, crc2, len2)); + } + + @Deprecated + public static IRubyObject crc32_combine(IRubyObject recv, IRubyObject arg0, IRubyObject arg1, IRubyObject arg2) { + return crc32_combine(((RubyBasicObject) recv).getCurrentContext(), recv, arg0, arg1, arg2); + } + + @JRubyMethod(name = "adler32_combine", module = true, visibility = PRIVATE) + public static IRubyObject adler32_combine(ThreadContext context, IRubyObject recv, + IRubyObject arg0, IRubyObject arg1, IRubyObject arg2) { + long adler1 = toLong(context, arg0); + long adler2 = toLong(context, arg1); + long len2 = toLong(context, arg2); + + return asFixnum(context, com.jcraft.jzlib.JZlib.adler32_combine(adler1, adler2, len2)); + } + + @Deprecated + public static IRubyObject adler32_combine(IRubyObject recv, IRubyObject arg0, IRubyObject arg1, IRubyObject arg2) { + return adler32_combine(((RubyBasicObject) recv).getCurrentContext(), recv, arg0, arg1, arg2); + } + + static RaiseException newZlibError(ThreadContext context, String message) { + return newZlibError(context, "Error", message); + } + + static RaiseException newBufError(ThreadContext context, String message) { + return newZlibError(context, "BufError", message); + } + + static RaiseException newDictError(ThreadContext context, String message) { + return newZlibError(context, "NeedDict", message); + } + + static RaiseException newStreamError(ThreadContext context, String message) { + return newZlibError(context, "StreamError", message); + } + + static RaiseException newDataError(ThreadContext context, String message) { + return newZlibError(context, "DataError", message); + } + + static RaiseException newZlibError(ThreadContext context, String klass, String message) { + return RaiseException.from(context.runtime, Access.getClass(context, "Zlib", klass), message); + } + + static RaiseException newGzipFileError(ThreadContext context, String message) { + return newGzipFileError(context, "Error", message); + } + + static RaiseException newCRCError(ThreadContext context, String message) { + return newGzipFileError(context, "CRCError", message); + } + + static RaiseException newNoFooter(ThreadContext context, String message) { + return newGzipFileError(context, "NoFooter", message); + } + + static RaiseException newLengthError(ThreadContext context, String message) { + return newGzipFileError(context, "LengthError", message); + } + + static RaiseException newGzipFileError(ThreadContext context, String klass, String message) { + RubyClass errorClass = Access.getClass(context, "Zlib", "GzipFile", klass); + RubyException excn = RubyException.newException(context.runtime, errorClass, message); + // TODO: not yet supported. rewrite GzipReader/Writer with Inflate/Deflate? + excn.setInstanceVariable("@input", context.nil); + return excn.toThrowable(); + } + + static int FIXNUMARG(ThreadContext context, IRubyObject obj, int ifnil) { + return obj.isNil() ? ifnil : toInt(context, obj); + } +} diff --git a/ext/java/org/jruby/ext/zlib/ZStream.java b/ext/java/org/jruby/ext/zlib/ZStream.java new file mode 100644 index 0000000..eb477c2 --- /dev/null +++ b/ext/java/org/jruby/ext/zlib/ZStream.java @@ -0,0 +1,295 @@ +/* + **** BEGIN LICENSE BLOCK ***** + * Version: EPL 2.0/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Eclipse Public + * License Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.eclipse.org/legal/epl-v20.html + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing + * rights and limitations under the License. + * + * Alternatively, the contents of this file may be used under the terms of + * either of the GNU General Public License Version 2 or later (the "GPL"), + * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the EPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the EPL, the GPL or the LGPL. + ***** END LICENSE BLOCK *****/ + +package org.jruby.ext.zlib; + +import com.jcraft.jzlib.JZlib; +import org.jruby.Ruby; +import org.jruby.RubyClass; +import org.jruby.RubyObject; +import org.jruby.RubyString; +import org.jruby.anno.JRubyClass; +import org.jruby.anno.JRubyMethod; +import org.jruby.runtime.Block; +import org.jruby.runtime.ThreadContext; + +import static org.jruby.api.Access.getModule; +import static org.jruby.api.Convert.asBoolean; +import static org.jruby.api.Convert.asFixnum; +import static org.jruby.api.Warn.warn; +import static org.jruby.runtime.Visibility.PRIVATE; +import org.jruby.runtime.builtin.IRubyObject; + +@JRubyClass(name = "Zlib::ZStream") +public abstract class ZStream extends RubyObject { + + protected boolean closed = false; + + protected abstract int internalTotalIn(); + + protected abstract int internalTotalOut(); + + protected abstract boolean internalStreamEndP(); + + @Deprecated(since = "10.0") + protected void internalReset() { + internalReset(getCurrentContext()); + } + + protected void internalReset(ThreadContext context) { + throw new RuntimeException("Missing internalReset Implementation"); + } + + protected abstract boolean internalFinished(); + + protected abstract long internalAdler(); + + @Deprecated(since = "10.0") + protected IRubyObject internalFinish(Block block) { + return internalFinish(getCurrentContext(), block); + } + + protected IRubyObject internalFinish(ThreadContext context, Block block) { + throw new RuntimeException("Missing internalFinish Implementation"); + } + + + protected abstract void internalClose(); + + public ZStream(Ruby runtime, RubyClass type) { + super(runtime, type); + } + + @JRubyMethod(visibility = PRIVATE) + public IRubyObject initialize(Block unusedBlock) { + return this; + } + + @JRubyMethod + public IRubyObject flush_next_out(ThreadContext context, Block block) { + return RubyString.newEmptyBinaryString(context.getRuntime()); + } + + @Deprecated + public IRubyObject total_out() { + return total_out(getCurrentContext()); + } + + @JRubyMethod + public IRubyObject total_out(ThreadContext context) { + checkClosed(context); + return asFixnum(context, internalTotalOut()); + } + + @Deprecated(since = "10.0") + public IRubyObject stream_end_p() { + return stream_end_p(getCurrentContext()); + } + + @JRubyMethod(name = "stream_end?") + public IRubyObject stream_end_p(ThreadContext context) { + return internalStreamEndP() ? context.tru : context.fals; + } + + @Deprecated(since = "10.0") + public IRubyObject data_type() { + return data_type(getCurrentContext()); + } + + @JRubyMethod(name = "data_type") + public IRubyObject data_type(ThreadContext context) { + checkClosed(context); + return getModule(context, "Zlib").getConstant(context, "UNKNOWN"); + } + + @Deprecated(since = "10.0") + public IRubyObject closed_p() { + return closed_p(getCurrentContext()); + } + + @JRubyMethod(name = {"closed?", "ended?"}) + public IRubyObject closed_p(ThreadContext context) { + return closed ? context.tru : context.fals; + } + + @Deprecated(since = "10.0") + public IRubyObject reset() { + return reset(getCurrentContext()); + } + + @JRubyMethod(name = "reset") + public IRubyObject reset(ThreadContext context) { + checkClosed(context); + internalReset(context); + + return context.nil; + } + + @Deprecated(since = "10.0") + public IRubyObject avail_out() { + return avail_out(getCurrentContext()); + } + + @JRubyMethod(name = "avail_out") + public IRubyObject avail_out(ThreadContext context) { + return asFixnum(context, 0); + } + + @Deprecated(since = "10.0") + public IRubyObject set_avail_out(IRubyObject p1) { + return set_avail_out(getCurrentContext(), p1); + } + + @JRubyMethod(name = "avail_out=") + public IRubyObject set_avail_out(ThreadContext context, IRubyObject p1) { + checkClosed(context); + + return p1; + } + + @JRubyMethod(name = "adler") + public IRubyObject adler(ThreadContext context) { + checkClosed(context); + + return asFixnum(context, internalAdler()); + } + + @Deprecated + public IRubyObject adler() { + return adler(getCurrentContext()); + } + + @JRubyMethod(name = "finish") + public IRubyObject finish(ThreadContext context, Block block) { + checkClosed(context); + + IRubyObject result = internalFinish(context, block); + + return result; + } + + @Deprecated(since = "10.0") + public IRubyObject avail_in() { + return avail_in(getCurrentContext()); + } + + @JRubyMethod(name = "avail_in") + public IRubyObject avail_in(ThreadContext context) { + return asFixnum(context, 0); + } + + @JRubyMethod(name = "flush_next_in") + public IRubyObject flush_next_in(ThreadContext context) { + return RubyString.newEmptyBinaryString(context.getRuntime()); + } + + @Deprecated + public IRubyObject total_in() { + return total_in(getCurrentContext()); + } + + @JRubyMethod(name = "total_in") + public IRubyObject total_in(ThreadContext context) { + checkClosed(context); + return asFixnum(context, internalTotalIn()); + } + + @JRubyMethod(name = "finished?") + public IRubyObject finished_p(ThreadContext context) { + checkClosed(context); + return asBoolean(context, internalFinished()); + } + + @Deprecated(since = "10.0") + public IRubyObject close() { + return close(getCurrentContext()); + } + + @JRubyMethod(name = {"close", "end"}) + public IRubyObject close(ThreadContext context) { + checkClosed(context); + internalClose(); + closed = true; + return context.nil; + } + + @Deprecated(since = "10.0") + void checkClosed() { + checkClosed(getCurrentContext()); + } + + void checkClosed(ThreadContext context) { + if (closed) throw RubyZlib.newZlibError(context, "stream is not ready"); + } + + // TODO: remove when JZlib checks the given level + static int checkLevel(ThreadContext context, int level) { + if ((level < 0 || level > 9) && level != JZlib.Z_DEFAULT_COMPRESSION) { + throw RubyZlib.newStreamError(context, "stream error: invalid level"); + } + return level; + } + + /** + * We only do windowBits=15(32K buffer, LZ77 algorithm) since java.util.zip + * only allows it. NOTE: deflateInit2 of zlib.c also accepts MAX_WBITS + + * 16(gzip compression). inflateInit2 also accepts MAX_WBITS + 16(gzip + * decompression) and MAX_WBITS + 32(automatic detection of gzip and LZ77). + */ + // TODO: remove when JZlib checks the given windowBits + static int checkWindowBits(ThreadContext context, int value, boolean forInflate) { + int wbits = Math.abs(value); + if ((wbits & 0xf) < 8) { + throw RubyZlib.newStreamError(context, "stream error: invalid window bits"); + } + if ((wbits & 0xf) != 0xf) { + // windowBits < 15 for reducing memory is meaningless on Java platform. + warn(context, "windowBits < 15 is ignored on this platform"); + // continue + } + if (forInflate && wbits > JZlib.MAX_WBITS + 32) { + throw RubyZlib.newStreamError(context, "stream error: invalid window bits"); + } else if (!forInflate && wbits > JZlib.MAX_WBITS + 16) { + throw RubyZlib.newStreamError(context, "stream error: invalid window bits"); + } + + return value; + } + + @Deprecated(since = "10.0") + static void checkStrategy(Ruby runtime, int strategy) { + checkStrategy(runtime.getCurrentContext(), strategy); + } + + // TODO: remove when JZlib checks the given strategy + static void checkStrategy(ThreadContext context, int strategy) { + switch (strategy) { + case JZlib.Z_DEFAULT_STRATEGY, JZlib.Z_FILTERED, JZlib.Z_HUFFMAN_ONLY -> {} + default -> throw RubyZlib.newStreamError(context, "stream error: invalid strategy"); + } + } +} diff --git a/ext/java/org/jruby/ext/zlib/Zlib.java b/ext/java/org/jruby/ext/zlib/Zlib.java new file mode 100644 index 0000000..fcc717e --- /dev/null +++ b/ext/java/org/jruby/ext/zlib/Zlib.java @@ -0,0 +1,54 @@ +/* + **** BEGIN LICENSE BLOCK ***** + * Version: EPL 2.0/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Eclipse Public + * License Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.eclipse.org/legal/epl-v20.html + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing + * rights and limitations under the License. + * + * Alternatively, the contents of this file may be used under the terms of + * either of the GNU General Public License Version 2 or later (the "GPL"), + * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the EPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the EPL, the GPL or the LGPL. + ***** END LICENSE BLOCK *****/ + +package org.jruby.ext.zlib; + +public final class Zlib { + private Zlib() {} + + // TODO: Those are defined at com.jcraft.jzlib.Deflate but private + public static final byte Z_BINARY = (byte) 0; + public static final byte Z_ASCII = (byte) 1; + public static final byte Z_UNKNOWN = (byte) 2; + // os_code + public static final byte OS_MSDOS = (byte) 0x00; + public static final byte OS_AMIGA = (byte) 0x01; + public static final byte OS_VMS = (byte) 0x02; + public static final byte OS_UNIX = (byte) 0x03; + public static final byte OS_ATARI = (byte) 0x05; + public static final byte OS_OS2 = (byte) 0x06; + public static final byte OS_MACOS = (byte) 0x07; + public static final byte OS_TOPS20 = (byte) 0x0a; + public static final byte OS_WIN32 = (byte) 0x0b; + public static final byte OS_VMCMS = (byte) 0x04; + public static final byte OS_ZSYSTEM = (byte) 0x08; + public static final byte OS_CPM = (byte) 0x09; + public static final byte OS_QDOS = (byte) 0x0c; + public static final byte OS_RISCOS = (byte) 0x0d; + public static final byte OS_UNKNOWN = (byte) 0xff; + public static final byte OS_CODE = OS_WIN32; // TODO: why we define OS_CODE to OS_WIN32? +} \ No newline at end of file diff --git a/ext/java/org/jruby/ext/zlib/ZlibLibrary.java b/ext/java/org/jruby/ext/zlib/ZlibLibrary.java new file mode 100644 index 0000000..8a0f3cd --- /dev/null +++ b/ext/java/org/jruby/ext/zlib/ZlibLibrary.java @@ -0,0 +1,39 @@ +/***** BEGIN LICENSE BLOCK ***** + * Version: EPL 2.0/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Eclipse Public + * License Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.eclipse.org/legal/epl-v20.html + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing + * rights and limitations under the License. + * + * Copyright (C) 2006 Ola Bini + * + * Alternatively, the contents of this file may be used under the terms of + * either of the GNU General Public License Version 2 or later (the "GPL"), + * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the EPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the EPL, the GPL or the LGPL. + ***** END LICENSE BLOCK *****/ + +package org.jruby.ext.zlib; + +import org.jruby.Ruby; +import org.jruby.runtime.load.Library; + +public class ZlibLibrary implements Library { + public void load(final Ruby runtime, boolean wrap) { + var context = runtime.getCurrentContext(); + RubyZlib.createZlibModule(context); + } +} diff --git a/zlib.gemspec b/zlib.gemspec index 345dc5f..851f106 100644 --- a/zlib.gemspec +++ b/zlib.gemspec @@ -11,6 +11,11 @@ source_version = ["", "ext/zlib/"].find do |dir| end end +version_module = Module.new do + version_rb = File.join(__dir__, "ext/java/lib/zlib/versions.rb") + module_eval(File.read(version_rb), version_rb) +end + Gem::Specification.new do |spec| spec.name = "zlib" spec.version = source_version @@ -22,10 +27,32 @@ Gem::Specification.new do |spec| spec.homepage = "https://github.com/ruby/zlib" spec.licenses = ["Ruby", "BSD-2-Clause"] - spec.files = ["COPYING", "BSDL", "README.md", "ext/zlib/extconf.rb", "ext/zlib/zlib.c", "zlib.gemspec"] + spec.files = ["COPYING", "BSDL", "README.md", "zlib.gemspec"] spec.bindir = "exe" spec.executables = [] - spec.require_paths = ["lib"] - spec.extensions = "ext/zlib/extconf.rb" + if RUBY_ENGINE == 'jruby' + spec.platform = 'java' + spec.files.concat %w[ + ext/java/org/jruby/ext/zlib/JZlibDeflate.java + ext/java/org/jruby/ext/zlib/JZlibInflate.java + ext/java/org/jruby/ext/zlib/JZlibRubyGzipReader.java + ext/java/org/jruby/ext/zlib/JZlibRubyGzipWriter.java + ext/java/org/jruby/ext/zlib/RubyGzipFile.java + ext/java/org/jruby/ext/zlib/RubyZlib.java + ext/java/org/jruby/ext/zlib/Zlib.java + ext/java/org/jruby/ext/zlib/ZlibLibrary.java + ext/java/org/jruby/ext/zlib/ZStream.java + ext/java/lib/zlib.rb + ext/java/lib/zlib.jar + ext/java/lib/zlib/versions.rb + ] + spec.require_paths = ["ext/java/lib"] + spec.requirements = "jar org.jruby:jzlib, #{version_module::Zlib::JZLIB_VERSION}" + spec.add_dependency "jar-dependencies", ">= 0.1.7" + else + spec.files.concat ["ext/zlib/extconf.rb", "ext/zlib/zlib.c"] + spec.require_paths = ["lib"] + spec.extensions = "ext/zlib/extconf.rb" + end spec.required_ruby_version = ">= 2.5.0" end