diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index f42d005..8b8549e 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -12,7 +12,7 @@ jobs: os: - ubuntu-20.04 - ubuntu-18.04 - ruby: [ '3.0', '2.7', '2.6', 'debug', 'head' ] + ruby: [ '3.0', '2.7', '2.6', 'debug', 'head', 'jruby-head' ] steps: - uses: actions/checkout@v3 - name: Set up Ruby @@ -22,3 +22,5 @@ jobs: bundler-cache: true # runs 'bundle install' and caches installed gems automatically - name: Run test run: bundle exec rake + - name: Build gem + run: gem install rake-compiler && rake build diff --git a/.gitignore b/.gitignore index bfa37d2..3c7a315 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ *.bundle *.dll *.so +*.jar Makefile diff --git a/Gemfile b/Gemfile index 4778872..c95a439 100644 --- a/Gemfile +++ b/Gemfile @@ -3,4 +3,8 @@ source "https://rubygems.org" gemspec gem 'rake-compiler' -gem 'test-unit' + +group :development do + gem 'test-unit' + gem 'ruby-maven', :platforms => :jruby +end diff --git a/Rakefile b/Rakefile index 296bdaf..0fe7f93 100644 --- a/Rakefile +++ b/Rakefile @@ -3,9 +3,19 @@ require "rake/testtask" name = "stringio" -require 'rake/extensiontask' -extask = Rake::ExtensionTask.new(name) do |x| - x.lib_dir << "/#{RUBY_VERSION}/#{x.platform}" +if RUBY_PLATFORM =~ /java/ + require 'rake/javaextensiontask' + extask = Rake::JavaExtensionTask.new("stringio") do |ext| + require 'maven/ruby/maven' + ext.source_version = '1.8' + ext.target_version = '1.8' + ext.ext_dir = 'ext/java' + end +else + require 'rake/extensiontask' + extask = Rake::ExtensionTask.new(name) do |x| + x.lib_dir << "/#{RUBY_VERSION}/#{x.platform}" + end end Rake::TestTask.new(:test) do |t| ENV["RUBYOPT"] = "-I" + [extask.lib_dir, "test/lib"].join(File::PATH_SEPARATOR) diff --git a/ext/java/org/jruby/ext/stringio/StringIO.java b/ext/java/org/jruby/ext/stringio/StringIO.java new file mode 100644 index 0000000..93a2287 --- /dev/null +++ b/ext/java/org/jruby/ext/stringio/StringIO.java @@ -0,0 +1,1507 @@ +/***** 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 Ryan Bell + * Copyright (C) 2007 Thomas E Enebo + * Copyright (C) 2008 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.stringio; + +import org.jcodings.Encoding; +import org.jcodings.specific.ASCIIEncoding; +import org.jruby.*; +import org.jruby.anno.FrameField; +import org.jruby.anno.JRubyClass; +import org.jruby.anno.JRubyMethod; +import org.jruby.ast.util.ArgsUtil; +import org.jruby.java.addons.IOJavaAddons; +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.runtime.encoding.EncodingCapable; +import org.jruby.runtime.marshal.DataType; +import org.jruby.util.ArraySupport; +import org.jruby.util.ByteList; +import org.jruby.util.StringSupport; +import org.jruby.util.TypeConverter; +import org.jruby.util.io.EncodingUtils; +import org.jruby.util.io.Getline; +import org.jruby.util.io.ModeFlags; +import org.jruby.util.io.OpenFile; + +import java.util.Arrays; + +import static org.jruby.RubyEnumerator.enumeratorize; +import static org.jruby.runtime.Visibility.PRIVATE; + +@JRubyClass(name="StringIO") +public class StringIO extends RubyObject implements EncodingCapable, DataType { + static class StringIOData { + /** + * ATTN: the value of internal might be reset to null + * (during StringIO.open with block), so watch out for that. + */ + RubyString string; + Encoding enc; + int pos; + int lineno; + int flags; + } + StringIOData ptr; + + private static final int STRIO_READABLE = ObjectFlags.STRIO_READABLE; + private static final int STRIO_WRITABLE = ObjectFlags.STRIO_WRITABLE; + private static final int STRIO_READWRITE = (STRIO_READABLE | STRIO_WRITABLE); + + public static RubyClass createStringIOClass(final Ruby runtime) { + RubyClass stringIOClass = runtime.defineClass( + "StringIO", runtime.getObject(), StringIO::new); + + stringIOClass.defineAnnotatedMethods(StringIO.class); + stringIOClass.includeModule(runtime.getEnumerable()); + + if (runtime.getObject().isConstantDefined("Java")) { + stringIOClass.defineAnnotatedMethods(IOJavaAddons.AnyIO.class); + } + + RubyModule genericReadable = runtime.getIO().defineOrGetModuleUnder("GenericReadable"); + genericReadable.defineAnnotatedMethods(GenericReadable.class); + stringIOClass.includeModule(genericReadable); + + RubyModule genericWritable = runtime.getIO().defineOrGetModuleUnder("GenericWritable"); + genericWritable.defineAnnotatedMethods(GenericWritable.class); + stringIOClass.includeModule(genericWritable); + + return stringIOClass; + } + + // mri: get_enc + public Encoding getEncoding() { + return ptr.enc != null ? ptr.enc : ptr.string.getEncoding(); + } + + public void setEncoding(Encoding enc) { + ptr.enc = enc; + } + + @JRubyMethod(name = "new", rest = true, meta = true) + public static IRubyObject newInstance(ThreadContext context, IRubyObject recv, IRubyObject[] args, Block block) { + return RubyIO.newInstance(context, recv, args, block); + } + + @JRubyMethod(meta = true, rest = true) + public static IRubyObject open(ThreadContext context, IRubyObject recv, IRubyObject[] args, Block block) { + StringIO strio = (StringIO)((RubyClass)recv).newInstance(context, args, Block.NULL_BLOCK); + IRubyObject val = strio; + + if (block.isGiven()) { + try { + val = block.yield(context, strio); + } finally { + strio.ptr.string = null; + strio.flags &= ~STRIO_READWRITE; + } + } + return val; + } + + protected StringIO(Ruby runtime, RubyClass klass) { + super(runtime, klass); + } + + @JRubyMethod(optional = 2, visibility = PRIVATE) + public IRubyObject initialize(ThreadContext context, IRubyObject[] args) { + if (ptr == null) { + ptr = new StringIOData(); + } + + // does not dispatch quite right and is not really necessary for us + //Helpers.invokeSuper(context, this, metaClass, "initialize", IRubyObject.NULL_ARRAY, Block.NULL_BLOCK); + strioInit(context, args); + return this; + } + + // MRI: strio_init + private void strioInit(ThreadContext context, IRubyObject[] args) { + Ruby runtime = context.runtime; + RubyString string; + IRubyObject mode; + + StringIOData ptr = this.ptr; + + synchronized (ptr) { + switch (args.length) { + case 2: + mode = args[1]; + final boolean trunc; + if (mode instanceof RubyFixnum) { + int flags = RubyFixnum.fix2int(mode); + ptr.flags = ModeFlags.getOpenFileFlagsFor(flags); + trunc = (flags & ModeFlags.TRUNC) != 0; + } else { + String m = args[1].convertToString().toString(); + ptr.flags = OpenFile.ioModestrFmode(runtime, m); + trunc = m.length() > 0 && m.charAt(0) == 'w'; + } + string = args[0].convertToString(); + if ((ptr.flags & OpenFile.WRITABLE) != 0 && string.isFrozen()) { + throw runtime.newErrnoEACCESError("Permission denied"); + } + if (trunc) { + string.resize(0); + } + break; + case 1: + string = args[0].convertToString(); + ptr.flags = string.isFrozen() ? OpenFile.READABLE : OpenFile.READWRITE; + break; + case 0: + string = RubyString.newEmptyString(runtime, runtime.getDefaultExternalEncoding()); + ptr.flags = OpenFile.READWRITE; + break; + default: + throw runtime.newArgumentError(args.length, 2); + } + + ptr.string = string; + ptr.enc = null; + ptr.pos = 0; + ptr.lineno = 0; + // funky way of shifting readwrite flags into object flags + flags |= (ptr.flags & OpenFile.READWRITE) * (STRIO_READABLE / OpenFile.READABLE); + } + } + + // MRI: strio_copy + @JRubyMethod(visibility = PRIVATE) + public IRubyObject initialize_copy(ThreadContext context, IRubyObject other) { + StringIO otherIO = (StringIO) TypeConverter.convertToType(other, + context.runtime.getClass("StringIO"), "to_strio"); + + if (this == otherIO) return this; + + ptr = otherIO.ptr; + flags &= ~STRIO_READWRITE; + flags |= otherIO.flags & STRIO_READWRITE; + + return this; + } + + @JRubyMethod + public IRubyObject binmode(ThreadContext context) { + ptr.enc = EncodingUtils.ascii8bitEncoding(context.runtime); + if (writable()) ptr.string.setEncoding(ptr.enc); + + return this; + } + + @JRubyMethod(name = "flush") + public IRubyObject strio_self() { + return this; + } + + @JRubyMethod(name = {"fcntl"}, rest = true) + public IRubyObject strio_unimpl(ThreadContext context, IRubyObject[] args) { + throw context.runtime.newNotImplementedError(""); + } + + @JRubyMethod(name = {"fsync"}) + public IRubyObject strioZero(ThreadContext context) { + return RubyFixnum.zero(context.runtime); + } + + @JRubyMethod(name = {"sync="}) + public IRubyObject strioFirst(IRubyObject arg) { + checkInitialized(); + return arg; + } + + @JRubyMethod(name = {"isatty", "tty?"}) + public IRubyObject strioFalse(ThreadContext context) { + return context.fals; + } + + @JRubyMethod(name = {"pid", "fileno"}) + public IRubyObject strioNil(ThreadContext context) { + return context.nil; + } + + @JRubyMethod + public IRubyObject close(ThreadContext context) { + checkInitialized(); + if ( closed() ) return context.nil; + + // NOTE: This is 2.0 behavior to allow dup'ed StringIO to remain open when original is closed + flags &= ~STRIO_READWRITE; + + return context.nil; + } + + @JRubyMethod(name = "closed?") + public IRubyObject closed_p() { + checkInitialized(); + return getRuntime().newBoolean(closed()); + } + + @JRubyMethod + public IRubyObject close_read(ThreadContext context) { + // ~ checkReadable() : + checkInitialized(); + if ( (ptr.flags & OpenFile.READABLE) == 0 ) { + throw context.runtime.newIOError("not opened for reading"); + } + if ( ( flags & STRIO_READABLE ) != 0 ) { + flags &= ~STRIO_READABLE; + } + return context.nil; + } + + @JRubyMethod(name = "closed_read?") + public IRubyObject closed_read_p() { + checkInitialized(); + return getRuntime().newBoolean(!readable()); + } + + @JRubyMethod + public IRubyObject close_write(ThreadContext context) { + // ~ checkWritable() : + checkInitialized(); + if ( (ptr.flags & OpenFile.WRITABLE) == 0 ) { + throw context.runtime.newIOError("not opened for writing"); + } + if ( ( flags & STRIO_WRITABLE ) != 0 ) { + flags &= ~STRIO_WRITABLE; + } + return context.nil; + } + + @JRubyMethod(name = "closed_write?") + public IRubyObject closed_write_p() { + checkInitialized(); + return getRuntime().newBoolean(!writable()); + } + + // MRI: strio_each + @JRubyMethod(name = "each", writes = FrameField.LASTLINE) + public IRubyObject each(ThreadContext context, Block block) { + if (!block.isGiven()) return enumeratorize(context.runtime, this, "each"); + + return Getline.getlineCall(context, GETLINE_YIELD, this, getEncoding(), 0, null, null, null, block); + } + + // MRI: strio_each + @JRubyMethod(name = "each", writes = FrameField.LASTLINE) + public IRubyObject each(ThreadContext context, IRubyObject arg0, Block block) { + if (!block.isGiven()) return enumeratorize(context.runtime, this, "each", arg0); + + return Getline.getlineCall(context, GETLINE_YIELD, this, getEncoding(), 1, arg0, null, null, block); + } + + // MRI: strio_each + @JRubyMethod(name = "each", writes = FrameField.LASTLINE) + public IRubyObject each(ThreadContext context, IRubyObject arg0, IRubyObject arg1, Block block) { + if (!block.isGiven()) return enumeratorize(context.runtime, this, "each", Helpers.arrayOf(arg0, arg1)); + + return Getline.getlineCall(context, GETLINE_YIELD, this, getEncoding(), 2, arg0, arg1, null, block); + } + + // MRI: strio_each + @JRubyMethod(name = "each") + public IRubyObject each(ThreadContext context, IRubyObject arg0, IRubyObject arg1, IRubyObject arg2, Block block) { + if (!block.isGiven()) return enumeratorize(context.runtime, this, "each", Helpers.arrayOf(arg0, arg1, arg2)); + + return Getline.getlineCall(context, GETLINE_YIELD, this, getEncoding(), 3, arg0, arg1, arg2, block); + } + + public IRubyObject each(ThreadContext context, IRubyObject[] args, Block block) { + if (!block.isGiven()) return enumeratorize(context.runtime, this, "each", args); + switch (args.length) { + case 0: + return each(context, block); + case 1: + return each(context, args[0], block); + case 2: + return each(context, args[0], args[1], block); + case 3: + return each(context, args[0], args[1], args[2], block); + default: + Arity.raiseArgumentError(context, args.length, 0, 3); + throw new AssertionError("BUG"); + } + } + + @JRubyMethod(name = "each_line") + public IRubyObject each_line(ThreadContext context, Block block) { + if (!block.isGiven()) return enumeratorize(context.runtime, this, "each_line"); + + return each(context, block); + } + + @JRubyMethod(name = "each_line") + public IRubyObject each_line(ThreadContext context, IRubyObject arg0, Block block) { + if (!block.isGiven()) return enumeratorize(context.runtime, this, "each_line", arg0); + + return each(context, arg0, block); + } + + @JRubyMethod(name = "each_line") + public IRubyObject each_line(ThreadContext context, IRubyObject arg0, IRubyObject arg1, Block block) { + if (!block.isGiven()) return enumeratorize(context.runtime, this, "each_line", arg0, arg1); + + return each(context, arg0, arg1, block); + } + + @JRubyMethod(name = "each_line") + public IRubyObject each_line(ThreadContext context, IRubyObject arg0, IRubyObject arg1, IRubyObject arg2, Block block) { + if (!block.isGiven()) return enumeratorize(context.runtime, this, "each_line", arg0, arg1, arg2); + + return each(context, arg0, arg1, arg2, block); + } + + public IRubyObject each_line(ThreadContext context, IRubyObject[] args, Block block) { + if (!block.isGiven()) return enumeratorize(context.runtime, this, "each_line", args); + switch (args.length) { + case 0: + return each_line(context, block); + case 1: + return each_line(context, args[0], block); + case 2: + return each_line(context, args[0], args[1], block); + case 3: + return each_line(context, args[0], args[1], args[2], block); + default: + Arity.raiseArgumentError(context, args.length, 0, 3); + throw new AssertionError("BUG"); + } + } + + @JRubyMethod(name = "lines", optional = 2) + public IRubyObject lines(ThreadContext context, IRubyObject[] args, Block block) { + context.runtime.getWarnings().warn("StringIO#lines is deprecated; use #each_line instead"); + return block.isGiven() ? each(context, args, block) : enumeratorize(context.runtime, this, "each_line", args); + } + + @JRubyMethod(name = {"each_byte", "bytes"}) + public IRubyObject each_byte(ThreadContext context, Block block) { + if (!block.isGiven()) return enumeratorize(context.runtime, this, "each_byte"); + + checkReadable(); + + Ruby runtime = context.runtime; + StringIOData ptr = this.ptr; + + synchronized (ptr) { + ByteList bytes = ptr.string.getByteList(); + + // Check the length every iteration, since + // the block can modify this string. + while (ptr.pos < bytes.length()) { + block.yield(context, runtime.newFixnum(bytes.get(ptr.pos++) & 0xFF)); + } + } + + return this; + } + + @JRubyMethod + public IRubyObject each_char(final ThreadContext context, final Block block) { + if (!block.isGiven()) return enumeratorize(context.runtime, this, "each_char"); + + IRubyObject c; + while (!(c = getc(context)).isNil()) { + block.yieldSpecific(context, c); + } + return this; + } + + @JRubyMethod + public IRubyObject chars(final ThreadContext context, final Block block) { + context.runtime.getWarnings().warn("StringIO#chars is deprecated; use #each_char instead"); + + return each_char(context, block); + } + + @JRubyMethod(name = {"eof", "eof?"}) + public IRubyObject eof(ThreadContext context) { + checkReadable(); + if (ptr.pos < ptr.string.size()) return context.fals; + return context.tru; + } + + private boolean isEndOfString() { + return ptr.pos >= ptr.string.size(); + } + + @JRubyMethod(name = "getc") + public IRubyObject getc(ThreadContext context) { + checkReadable(); + + if (isEndOfString()) return context.nil; + + StringIOData ptr = this.ptr; + + synchronized (ptr) { + int start = ptr.pos; + int total = 1 + StringSupport.bytesToFixBrokenTrailingCharacter(ptr.string.getByteList(), start + 1); + + ptr.pos += total; + + return context.runtime.newString(ptr.string.getByteList().makeShared(start, total)); + } + } + + @JRubyMethod(name = "getbyte") + public IRubyObject getbyte(ThreadContext context) { + checkReadable(); + + if (isEndOfString()) return context.nil; + + int c; + StringIOData ptr = this.ptr; + synchronized (ptr) { + c = ptr.string.getByteList().get(this.ptr.pos++) & 0xFF; + } + + return context.runtime.newFixnum(c); + } + + private RubyString strioSubstr(Ruby runtime, int pos, int len, Encoding enc) { + StringIOData ptr = this.ptr; + + synchronized (ptr) { + final RubyString string = ptr.string; + final ByteList stringBytes = string.getByteList(); + int rlen = string.size() - pos; + + if (len > rlen) len = rlen; + if (len < 0) len = 0; + + if (len == 0) return RubyString.newEmptyString(runtime, enc); + string.setByteListShared(); // we only share the byte[] buffer but its easier this way + return RubyString.newStringShared(runtime, stringBytes.getUnsafeBytes(), stringBytes.getBegin() + pos, len, enc); + } + } + + private static final int CHAR_BIT = 8; + + private static void bm_init_skip(int[] skip, byte[] pat, int patPtr, int m) { + int c; + + for (c = 0; c < (1 << CHAR_BIT); c++) { + skip[c] = m; + } + while ((--m) > 0) { + skip[pat[patPtr++]] = m; + } + } + + // Note that this is substantially more complex in 2.0 (Onigmo) + private static int bm_search(byte[] little, int lstart, int llen, byte[] big, int bstart, int blen, int[] skip) { + int i, j, k; + + i = llen - 1; + while (i < blen) { + k = i; + j = llen - 1; + while (j >= 0 && big[k + bstart] == little[j + lstart]) { + k--; + j--; + } + if (j < 0) return k + 1; + i += skip[big[i + bstart] & 0xFF]; + } + return -1; + } + + @JRubyMethod(name = "gets", writes = FrameField.LASTLINE) + public IRubyObject gets(ThreadContext context) { + return Getline.getlineCall(context, GETLINE, this, getEncoding()); + } + + @JRubyMethod(name = "gets", writes = FrameField.LASTLINE) + public IRubyObject gets(ThreadContext context, IRubyObject arg0) { + return Getline.getlineCall(context, GETLINE, this, getEncoding(), arg0); + } + + @JRubyMethod(name = "gets", writes = FrameField.LASTLINE) + public IRubyObject gets(ThreadContext context, IRubyObject arg0, IRubyObject arg1) { + return Getline.getlineCall(context, GETLINE, this, getEncoding(), arg0, arg1); + } + + @JRubyMethod(name = "gets", writes = FrameField.LASTLINE) + public IRubyObject gets(ThreadContext context, IRubyObject arg0, IRubyObject arg1, IRubyObject arg2) { + return Getline.getlineCall(context, GETLINE, this, getEncoding(), arg0, arg1, arg2); + } + + public IRubyObject gets(ThreadContext context, IRubyObject[] args) { + switch (args.length) { + case 0: + return gets(context); + case 1: + return gets(context, args[0]); + case 2: + return gets(context, args[0], args[1]); + case 3: + return gets(context, args[0], args[1], args[2]); + default: + Arity.raiseArgumentError(context, args.length, 0, 3); + throw new AssertionError("BUG"); + } + } + + private static final Getline.Callback GETLINE = new Getline.Callback() { + @Override + public IRubyObject getline(ThreadContext context, StringIO self, IRubyObject rs, int limit, boolean chomp, Block block) { + if (limit == 0) { + return RubyString.newEmptyString(context.runtime, self.getEncoding()); + } + + IRubyObject result = self.getline(context, rs, limit, chomp); + + context.setLastLine(result); + + return result; + } + }; + + private static final Getline.Callback GETLINE_YIELD = new Getline.Callback() { + @Override + public StringIO getline(ThreadContext context, StringIO self, IRubyObject rs, int limit, boolean chomp, Block block) { + IRubyObject line; + + if (limit == 0) { + throw context.runtime.newArgumentError("invalid limit: 0 for each_line"); + } + + while (!(line = self.getline(context, rs, limit, chomp)).isNil()) { + block.yieldSpecific(context, line); + } + + return self; + } + }; + + private static final Getline.Callback GETLINE_ARY = new Getline.Callback() { + @Override + public RubyArray getline(ThreadContext context, StringIO self, IRubyObject rs, int limit, boolean chomp, Block block) { + RubyArray ary = context.runtime.newArray(); + IRubyObject line; + + if (limit == 0) { + throw context.runtime.newArgumentError("invalid limit: 0 for readlines"); + } + + while (!(line = self.getline(context, rs, limit, chomp)).isNil()) { + ary.append(line); + } + + return ary; + } + }; + + // strio_getline + private IRubyObject getline(ThreadContext context, final IRubyObject rs, int limit, boolean chomp) { + Ruby runtime = context.runtime; + + RubyString str; + + checkReadable(); + + int n; + + if (isEndOfString()) { + return context.nil; + } + + StringIOData ptr = this.ptr; + Encoding enc = getEncoding(); + + synchronized (ptr) { + final ByteList string = ptr.string.getByteList(); + final byte[] stringBytes = string.getUnsafeBytes(); + int begin = string.getBegin(); + int s = begin + ptr.pos; + int e = begin + string.getRealSize(); + int p; + int w = 0; + + if (limit > 0 && s + limit < e) { + e = getEncoding().rightAdjustCharHead(stringBytes, s, s + limit, e); + } + if (rs == context.nil) { + if (chomp) { + w = chompNewlineWidth(stringBytes, s, e); + } + str = strioSubstr(runtime, ptr.pos, e - s - w, enc); + } else if ((n = ((RubyString) rs).size()) == 0) { + // this is not an exact port; the original confused me + // in MRI, the next loop appears to have a complicated boolean to determine the index, but in actuality + // it just resolves to p (+ 0) as below. We theorize that the MRI logic may have originally been + // intended to skip all \n and \r, but because p does not get incremented before the \r check that + // logic never fires. We use the original logic that did not have these strange flaws. + // See https://github.com/ruby/ruby/commit/30540c567569d3486ccbf59b59d903d5778f04d5 + p = s; + while (stringBytes[p] == '\n') { + if (++p == e) { + return context.nil; + } + } + s = p; + while ((p = StringSupport.memchr(stringBytes, p, '\n', e - p)) != -1 && (p != e)) { + p += 1; + if (p == e) break; + + if (stringBytes[p] == '\n') { + e = p + 1; + w = (chomp ? 1 : 0); + break; + } + else if (stringBytes[p] == '\r' && p < e && stringBytes[p + 1] == '\n') { + e = p + 2; + w = (chomp ? 2 : 0); + break; + } + } + if (w == 0 && chomp) { + w = chompNewlineWidth(stringBytes, s, e); + } + str = strioSubstr(runtime, s - begin, e - s - w, enc); + } else if (n == 1) { + RubyString strStr = (RubyString) rs; + ByteList strByteList = strStr.getByteList(); + if ((p = StringSupport.memchr(stringBytes, s, strByteList.get(0), e - s)) != -1) { + e = p + 1; + w = (chomp ? ((p > s && stringBytes[p-1] == '\r')?1:0) + 1 : 0); + } + str = strioSubstr(runtime, ptr.pos, e - s - w, enc); + } else { + if (n < e - s) { + RubyString rsStr = (RubyString) rs; + ByteList rsByteList = rsStr.getByteList(); + byte[] rsBytes = rsByteList.getUnsafeBytes(); + + int[] skip = new int[1 << CHAR_BIT]; + int pos; + p = rsByteList.getBegin(); + bm_init_skip(skip, rsBytes, p, n); + if ((pos = bm_search(rsBytes, p, n, stringBytes, s, e - s, skip)) >= 0) { + e = s + pos + n; + } + } + str = strioSubstr(runtime, ptr.pos, e - s - w, enc); + } + ptr.pos = e - begin; + ptr.lineno++; + } + + return str; + } + + private static int chompNewlineWidth(byte[] bytes, int s, int e) { + if (e > s && bytes[--e] == '\n') { + if (e > s && bytes[--e] == '\r') return 2; + return 1; + } + return 0; + } + + @JRubyMethod(name = {"length", "size"}) + public IRubyObject length() { + checkInitialized(); + checkFinalized(); + return getRuntime().newFixnum(ptr.string.size()); + } + + @JRubyMethod(name = "lineno") + public IRubyObject lineno(ThreadContext context) { + return context.runtime.newFixnum(ptr.lineno); + } + + @JRubyMethod(name = "lineno=", required = 1) + public IRubyObject set_lineno(ThreadContext context, IRubyObject arg) { + ptr.lineno = RubyNumeric.fix2int(arg); + + return context.nil; + } + + @JRubyMethod(name = {"pos", "tell"}) + public IRubyObject pos(ThreadContext context) { + checkInitialized(); + + return context.runtime.newFixnum(ptr.pos); + } + + @JRubyMethod(name = "pos=", required = 1) + public IRubyObject set_pos(IRubyObject arg) { + checkInitialized(); + + long p = RubyNumeric.fix2long(arg); + + if (p < 0) throw getRuntime().newErrnoEINVALError(arg.toString()); + + if (p > Integer.MAX_VALUE) throw getRuntime().newArgumentError("JRuby does not support StringIO larger than " + Integer.MAX_VALUE + " bytes"); + + ptr.pos = (int)p; + + return arg; + } + + private void strioExtend(int pos, int len) { + StringIOData ptr = this.ptr; + + synchronized (ptr) { + final int olen = ptr.string.size(); + if (pos + len > olen) { + ptr.string.resize(pos + len); + if (pos > olen) { + ptr.string.modify19(); + ByteList ptrByteList = ptr.string.getByteList(); + // zero the gap + Arrays.fill(ptrByteList.getUnsafeBytes(), + ptrByteList.getBegin() + olen, + ptrByteList.getBegin() + pos, + (byte) 0); + } + } else { + ptr.string.modify19(); + } + } + } + + // MRI: strio_putc + @JRubyMethod(name = "putc") + public IRubyObject putc(ThreadContext context, IRubyObject ch) { + Ruby runtime = context.runtime; + checkWritable(); + IRubyObject str; + + checkModifiable(); + if (ch instanceof RubyString) { + str = ((RubyString)ch).substr19(runtime, 0, 1); + } + else { + byte c = RubyNumeric.num2chr(ch); + str = RubyString.newString(runtime, new byte[]{c}); + } + write(context, str); + return ch; + } + + public static final ByteList NEWLINE = ByteList.create("\n"); + + @JRubyMethod(name = "read", optional = 2) + public IRubyObject read(ThreadContext context, IRubyObject[] args) { + checkReadable(); + + final Ruby runtime = context.runtime; + IRubyObject str = context.nil; + int len; + boolean binary = false; + + StringIOData ptr = this.ptr; + final RubyString string; + + synchronized (ptr) { + switch (args.length) { + case 2: + str = args[1]; + if (!str.isNil()) { + str = str.convertToString(); + ((RubyString) str).modify(); + } + case 1: + if (!args[0].isNil()) { + len = RubyNumeric.fix2int(args[0]); + + if (len < 0) { + throw runtime.newArgumentError("negative length " + len + " given"); + } + if (len > 0 && isEndOfString()) { + if (!str.isNil()) ((RubyString) str).resize(0); + return context.nil; + } + binary = true; + break; + } + case 0: + len = ptr.string.size(); + if (len <= ptr.pos) { + Encoding enc = binary ? ASCIIEncoding.INSTANCE : getEncoding(); + if (str.isNil()) { + str = runtime.newString(); + } else { + ((RubyString) str).resize(0); + } + ((RubyString) str).setEncoding(enc); + return str; + } else { + len -= ptr.pos; + } + break; + default: + throw runtime.newArgumentError(args.length, 0); + } + + if (str.isNil()) { + Encoding enc = binary ? ASCIIEncoding.INSTANCE : getEncoding(); + string = strioSubstr(runtime, ptr.pos, len, enc); + } else { + string = (RubyString) str; + int rest = ptr.string.size() - ptr.pos; + if (len > rest) len = rest; + string.resize(len); + ByteList strByteList = string.getByteList(); + byte[] strBytes = strByteList.getUnsafeBytes(); + ByteList dataByteList = ptr.string.getByteList(); + byte[] dataBytes = dataByteList.getUnsafeBytes(); + System.arraycopy(dataBytes, dataByteList.getBegin() + ptr.pos, strBytes, strByteList.getBegin(), len); + if (binary) { + string.setEncoding(ASCIIEncoding.INSTANCE); + } else { + string.setEncoding(ptr.string.getEncoding()); + } + } + ptr.pos += string.size(); + } + + return string; + } + + @JRubyMethod(name = "readlines") + public IRubyObject readlines(ThreadContext context) { + return Getline.getlineCall(context, GETLINE_ARY, this, getEncoding()); + } + + @JRubyMethod(name = "readlines") + public IRubyObject readlines(ThreadContext context, IRubyObject arg0) { + return Getline.getlineCall(context, GETLINE_ARY, this, getEncoding(), arg0); + } + + @JRubyMethod(name = "readlines") + public IRubyObject readlines(ThreadContext context, IRubyObject arg0, IRubyObject arg1) { + return Getline.getlineCall(context, GETLINE_ARY, this, getEncoding(), arg0, arg1); + } + + @JRubyMethod(name = "readlines") + public IRubyObject readlines(ThreadContext context, IRubyObject arg0, IRubyObject arg1, IRubyObject arg2) { + return Getline.getlineCall(context, GETLINE_ARY, this, getEncoding(), arg0, arg1, arg2); + } + + public IRubyObject readlines(ThreadContext context, IRubyObject[] args) { + switch (args.length) { + case 0: + return readlines(context); + case 1: + return readlines(context, args[0]); + case 2: + return readlines(context, args[0], args[1]); + case 3: + return readlines(context, args[0], args[1], args[2]); + default: + Arity.raiseArgumentError(context, args.length, 0, 3); + throw new AssertionError("BUG"); + } + } + + // MRI: strio_reopen + @JRubyMethod(name = "reopen", required = 0, optional = 2) + public IRubyObject reopen(ThreadContext context, IRubyObject[] args) { + checkFrozen(); + + if (args.length == 1 && !(args[0] instanceof RubyString)) { + return initialize_copy(context, args[0]); + } + + // reset the state + strioInit(context, args); + return this; + } + + @JRubyMethod(name = "rewind") + public IRubyObject rewind(ThreadContext context) { + checkInitialized(); + + StringIOData ptr = this.ptr; + + synchronized (ptr) { + ptr.pos = 0; + ptr.lineno = 0; + } + + return RubyFixnum.zero(context.runtime); + } + + @JRubyMethod(required = 1, optional = 1) + public IRubyObject seek(ThreadContext context, IRubyObject[] args) { + Ruby runtime = context.runtime; + + checkFrozen(); + checkFinalized(); + + int offset = RubyNumeric.num2int(args[0]); + IRubyObject whence = context.nil; + + if (args.length > 1 && !args[0].isNil()) whence = args[1]; + + checkOpen(); + + StringIOData ptr = this.ptr; + + synchronized (ptr) { + switch (whence.isNil() ? 0 : RubyNumeric.num2int(whence)) { + case 0: + break; + case 1: + offset += ptr.pos; + break; + case 2: + offset += ptr.string.size(); + break; + default: + throw runtime.newErrnoEINVALError("invalid whence"); + } + + if (offset < 0) throw runtime.newErrnoEINVALError("invalid seek value"); + + ptr.pos = offset; + } + + return RubyFixnum.zero(runtime); + } + + @JRubyMethod(name = "string=", required = 1) + public IRubyObject set_string(IRubyObject arg) { + checkFrozen(); + StringIOData ptr = this.ptr; + + synchronized (ptr) { + ptr.flags &= ~OpenFile.READWRITE; + RubyString str = arg.convertToString(); + ptr.flags = str.isFrozen() ? OpenFile.READABLE : OpenFile.READWRITE; + ptr.pos = 0; + ptr.lineno = 0; + return ptr.string = str; + } + } + + @JRubyMethod(name = "string") + public IRubyObject string(ThreadContext context) { + RubyString string = ptr.string; + if (string == null) return context.nil; + + return string; + } + + @JRubyMethod(name = "sync") + public IRubyObject sync(ThreadContext context) { + checkInitialized(); + return context.tru; + } + + // only here for the fake-out class in org.jruby + public IRubyObject sysread(IRubyObject[] args) { + return GenericReadable.sysread(getRuntime().getCurrentContext(), this, args); + } + + @JRubyMethod(name = "truncate", required = 1) + public IRubyObject truncate(IRubyObject len) { + checkWritable(); + + int l = RubyFixnum.fix2int(len); + StringIOData ptr = this.ptr; + RubyString string = ptr.string; + + synchronized (ptr) { + int plen = string.size(); + if (l < 0) { + throw getRuntime().newErrnoEINVALError("negative legnth"); + } + string.resize(l); + ByteList buf = string.getByteList(); + if (plen < l) { + // zero the gap + Arrays.fill(buf.getUnsafeBytes(), buf.getBegin() + plen, buf.getBegin() + l, (byte) 0); + } + } + + return len; + } + + @JRubyMethod(name = "ungetc") + public IRubyObject ungetc(ThreadContext context, IRubyObject arg) { + Encoding enc, enc2; + + checkModifiable(); + checkReadable(); + + if (arg.isNil()) return arg; + if (arg instanceof RubyInteger) { + int len, cc = RubyNumeric.num2int(arg); + byte[] buf = new byte[16]; + + enc = getEncoding(); + len = enc.codeToMbcLength(cc); + if (len <= 0) EncodingUtils.encUintChr(context, cc, enc); + enc.codeToMbc(cc, buf, 0); + ungetbyteCommon(buf, 0, len); + return context.nil; + } else { + arg = arg.convertToString(); + enc = getEncoding(); + RubyString argStr = (RubyString) arg; + enc2 = argStr.getEncoding(); + if (enc != enc2 && enc != ASCIIEncoding.INSTANCE) { + argStr = EncodingUtils.strConvEnc(context, argStr, enc2, enc); + } + ByteList argBytes = argStr.getByteList(); + ungetbyteCommon(argBytes.unsafeBytes(), argBytes.begin(), argBytes.realSize()); + return context.nil; + } + } + + private void ungetbyteCommon(int c) { + StringIOData ptr = this.ptr; + + synchronized (ptr) { + ptr.string.modify(); + ptr.pos--; + + ByteList bytes = ptr.string.getByteList(); + + if (isEndOfString()) bytes.length(ptr.pos + 1); + + if (ptr.pos == -1) { + bytes.prepend((byte) c); + ptr.pos = 0; + } else { + bytes.set(ptr.pos, c); + } + } + } + + private void ungetbyteCommon(RubyString ungetBytes) { + ByteList ungetByteList = ungetBytes.getByteList(); + ungetbyteCommon(ungetByteList.unsafeBytes(), ungetByteList.begin(), ungetByteList.realSize()); + } + + private void ungetbyteCommon(byte[] ungetBytes, int ungetBegin, int ungetLen) { + final int start; // = ptr.pos; + + if (ungetLen == 0) return; + + StringIOData ptr = this.ptr; + + synchronized (ptr) { + ptr.string.modify(); + + if (ungetLen > ptr.pos) { + start = 0; + } else { + start = ptr.pos - ungetLen; + } + + ByteList byteList = ptr.string.getByteList(); + + if (isEndOfString()) byteList.length(Math.max(ptr.pos, ungetLen)); + + byteList.replace(start, ptr.pos - start, ungetBytes, ungetBegin, ungetLen); + + ptr.pos = start; + } + } + + @JRubyMethod + public IRubyObject ungetbyte(ThreadContext context, IRubyObject arg) { + // TODO: Not a line-by-line port. + checkReadable(); + + if (arg.isNil()) return arg; + + checkModifiable(); + + if (arg instanceof RubyInteger) { + ungetbyteCommon(((RubyInteger) ((RubyInteger) arg).op_mod(context, 256)).getIntValue()); + } else { + ungetbyteCommon(arg.convertToString()); + } + + return context.nil; + } + + // MRI: strio_write + @JRubyMethod(name = "write") + public IRubyObject write(ThreadContext context, IRubyObject arg) { + Ruby runtime = context.runtime; + return RubyFixnum.newFixnum(runtime, stringIOWrite(context, runtime, arg)); + } + + @JRubyMethod(name = "write", required = 1, rest = true) + public IRubyObject write(ThreadContext context, IRubyObject[] args) { + Ruby runtime = context.runtime; + long len = 0; + for (IRubyObject arg : args) { + len += stringIOWrite(context, runtime, arg); + } + return RubyFixnum.newFixnum(runtime, len); + } + + // MRI: strio_write + private long stringIOWrite(ThreadContext context, Ruby runtime, IRubyObject arg) { + checkWritable(); + + RubyString str = arg.asString(); + int len, olen; + + StringIOData ptr = this.ptr; + + synchronized (ptr) { + final Encoding enc = getEncoding(); + final Encoding encStr = str.getEncoding(); + if (enc != encStr && enc != EncodingUtils.ascii8bitEncoding(runtime) + // this is a hack because we don't seem to handle incoming ASCII-8BIT properly in transcoder + && encStr != ASCIIEncoding.INSTANCE) { + str = EncodingUtils.strConvEnc(context, str, encStr, enc); + } + final ByteList strByteList = str.getByteList(); + len = str.size(); + if (len == 0) return 0; + checkModifiable(); + olen = ptr.string.size(); + if ((ptr.flags & OpenFile.APPEND) != 0) { + ptr.pos = olen; + } + if (ptr.pos == olen) { + if (enc == EncodingUtils.ascii8bitEncoding(runtime) || encStr == EncodingUtils.ascii8bitEncoding(runtime)) { + EncodingUtils.encStrBufCat(runtime, ptr.string, strByteList, enc); + } else { + ptr.string.cat19(str); + } + } else { + strioExtend(ptr.pos, len); + ByteList ptrByteList = ptr.string.getByteList(); + System.arraycopy(strByteList.getUnsafeBytes(), strByteList.getBegin(), ptrByteList.getUnsafeBytes(), ptrByteList.begin() + ptr.pos, len); + } + ptr.pos += len; + } + + return len; + } + + @JRubyMethod + public IRubyObject set_encoding(ThreadContext context, IRubyObject ext_enc) { + final Encoding enc; + if ( ext_enc.isNil() ) { + enc = EncodingUtils.defaultExternalEncoding(context.runtime); + } else { + enc = EncodingUtils.rbToEncoding(context, ext_enc); + } + + StringIOData ptr = this.ptr; + + synchronized (ptr) { + ptr.enc = enc; + + // in read-only mode, StringIO#set_encoding no longer sets the encoding + RubyString string; + if (writable() && (string = ptr.string).getEncoding() != enc) { + string.modify(); + string.setEncoding(enc); + } + } + + return this; + } + + @JRubyMethod + public IRubyObject set_encoding(ThreadContext context, IRubyObject enc, IRubyObject ignored) { + return set_encoding(context, enc); + } + + @JRubyMethod + public IRubyObject set_encoding(ThreadContext context, IRubyObject enc, IRubyObject ignored1, IRubyObject ignored2) { + return set_encoding(context, enc); + } + + @JRubyMethod + public IRubyObject external_encoding(ThreadContext context) { + return context.runtime.getEncodingService().convertEncodingToRubyEncoding(getEncoding()); + } + + @JRubyMethod + public IRubyObject internal_encoding(ThreadContext context) { + return context.nil; + } + + @JRubyMethod(name = "each_codepoint") + public IRubyObject each_codepoint(ThreadContext context, Block block) { + Ruby runtime = context.runtime; + + if (!block.isGiven()) return enumeratorize(runtime, this, "each_codepoint"); + + checkReadable(); + + StringIOData ptr = this.ptr; + + synchronized (ptr) { + final Encoding enc = getEncoding(); + final ByteList string = ptr.string.getByteList(); + final byte[] stringBytes = string.getUnsafeBytes(); + int begin = string.getBegin(); + for (; ; ) { + if (ptr.pos >= ptr.string.size()) return this; + + int c = StringSupport.codePoint(runtime, enc, stringBytes, begin + ptr.pos, stringBytes.length); + int n = StringSupport.codeLength(enc, c); + block.yield(context, runtime.newFixnum(c)); + ptr.pos += n; + } + } + } + + @JRubyMethod(name = "codepoints") + public IRubyObject codepoints(ThreadContext context, Block block) { + Ruby runtime = context.runtime; + runtime.getWarnings().warn("StringIO#codepoints is deprecated; use #each_codepoint"); + + if (!block.isGiven()) return enumeratorize(runtime, this, "each_codepoint"); + + return each_codepoint(context, block); + } + + public static class GenericReadable { + @JRubyMethod(name = "readchar") + public static IRubyObject readchar(ThreadContext context, IRubyObject self) { + IRubyObject c = self.callMethod(context, "getc"); + + if (c.isNil()) throw context.runtime.newEOFError(); + + return c; + } + + @JRubyMethod(name = "readbyte") + public static IRubyObject readbyte(ThreadContext context, IRubyObject self) { + IRubyObject b = self.callMethod(context, "getbyte"); + + if (b.isNil()) throw context.runtime.newEOFError(); + + return b; + } + + @JRubyMethod(name = "readline", optional = 1, writes = FrameField.LASTLINE) + public static IRubyObject readline(ThreadContext context, IRubyObject self, IRubyObject[] args) { + IRubyObject line = self.callMethod(context, "gets", args); + + if (line.isNil()) throw context.runtime.newEOFError(); + + return line; + } + + @JRubyMethod(name = {"sysread", "readpartial"}, optional = 2) + public static IRubyObject sysread(ThreadContext context, IRubyObject self, IRubyObject[] args) { + IRubyObject val = self.callMethod(context, "read", args); + + if (val.isNil()) throw context.runtime.newEOFError(); + + return val; + } + + @JRubyMethod(name = "read_nonblock", required = 1, optional = 2) + public static IRubyObject read_nonblock(ThreadContext context, IRubyObject self, IRubyObject[] args) { + final Ruby runtime = context.runtime; + + boolean exception = true; + IRubyObject opts = ArgsUtil.getOptionsArg(runtime, args); + if (opts != context.nil) { + args = ArraySupport.newCopy(args, args.length - 1); + exception = Helpers.extractExceptionOnlyArg(context, (RubyHash) opts); + } + + IRubyObject val = self.callMethod(context, "read", args); + if (val == context.nil) { + if (!exception) return context.nil; + throw runtime.newEOFError(); + } + + return val; + } + } + + public static class GenericWritable { + @JRubyMethod(name = "<<", required = 1) + public static IRubyObject append(ThreadContext context, IRubyObject self, IRubyObject arg) { + // Claims conversion is done via 'to_s' in docs. + self.callMethod(context, "write", arg); + + return self; + } + + @JRubyMethod(name = "print", rest = true, writes = FrameField.LASTLINE) + public static IRubyObject print(ThreadContext context, IRubyObject self, IRubyObject[] args) { + return RubyIO.print(context, self, args); + } + + @JRubyMethod(name = "printf", required = 1, rest = true) + public static IRubyObject printf(ThreadContext context, IRubyObject self, IRubyObject[] args) { + self.callMethod(context, "write", RubyKernel.sprintf(context, self, args)); + return context.nil; + } + + @JRubyMethod(name = "puts", rest = true) + public static IRubyObject puts(ThreadContext context, IRubyObject maybeIO, IRubyObject[] args) { + // TODO: This should defer to RubyIO logic, but we don't have puts right there for 1.9 + Ruby runtime = context.runtime; + if (args.length == 0) { + RubyIO.write(context, maybeIO, RubyString.newStringShared(runtime, NEWLINE)); + return runtime.getNil(); + } + + for (int i = 0; i < args.length; i++) { + RubyString line = null; + + if (!args[i].isNil()) { + IRubyObject tmp = args[i].checkArrayType(); + if (!tmp.isNil()) { + RubyArray arr = (RubyArray) tmp; + if (runtime.isInspecting(arr)) { + line = runtime.newString("[...]"); + } else { + inspectPuts(context, maybeIO, arr); + continue; + } + } else { + if (args[i] instanceof RubyString) { + line = (RubyString) args[i]; + } else { + line = args[i].asString(); + } + } + } + + if (line != null) RubyIO.write(context, maybeIO, line); + + if (line == null || !line.getByteList().endsWith(NEWLINE)) { + RubyIO.write(context, maybeIO, RubyString.newStringShared(runtime, NEWLINE)); + } + } + + return runtime.getNil(); + } + + private static IRubyObject inspectPuts(ThreadContext context, IRubyObject maybeIO, RubyArray array) { + Ruby runtime = context.runtime; + try { + runtime.registerInspecting(array); + return puts(context, maybeIO, array.toJavaArray()); + } + finally { + runtime.unregisterInspecting(array); + } + } + + @JRubyMethod(name = "syswrite", required = 1) + public static IRubyObject syswrite(ThreadContext context, IRubyObject self, IRubyObject arg) { + return RubyIO.write(context, self, arg); + } + + @JRubyMethod(name = "write_nonblock", required = 1, optional = 1) + public static IRubyObject syswrite_nonblock(ThreadContext context, IRubyObject self, IRubyObject[] args) { + Ruby runtime = context.runtime; + + ArgsUtil.getOptionsArg(runtime, args); // ignored as in MRI + + return syswrite(context, self, args[0]); + } + } + + public IRubyObject puts(ThreadContext context, IRubyObject[] args) { + return GenericWritable.puts(context, this, args); + } + + /* rb: check_modifiable */ + public void checkFrozen() { + super.checkFrozen(); + checkInitialized(); + } + + private boolean readable() { + return (flags & STRIO_READABLE) != 0 + && (ptr.flags & OpenFile.READABLE) != 0; + } + + private boolean writable() { + return (flags & STRIO_WRITABLE) != 0 + && (ptr.flags & OpenFile.WRITABLE) != 0; + } + + private boolean closed() { + return !((flags & STRIO_READWRITE) != 0 + && (ptr.flags & OpenFile.READWRITE) != 0); + } + + /* rb: readable */ + private void checkReadable() { + checkInitialized(); + if (!readable()) { + throw getRuntime().newIOError("not opened for reading"); + } + } + + /* rb: writable */ + private void checkWritable() { + checkInitialized(); + if (!writable()) { + throw getRuntime().newIOError("not opened for writing"); + } + + // Tainting here if we ever want it. (secure 4) + } + + private void checkModifiable() { + checkFrozen(); + if (ptr.string.isFrozen()) throw getRuntime().newIOError("not modifiable string"); + } + + private void checkInitialized() { + if (ptr == null) { + throw getRuntime().newIOError("uninitialized stream"); + } + } + + private void checkFinalized() { + if (ptr.string == null) { + throw getRuntime().newIOError("not opened"); + } + } + + private void checkOpen() { + if (closed()) { + throw getRuntime().newIOError(RubyIO.CLOSED_STREAM_MSG); + } + } +} diff --git a/ext/java/org/jruby/ext/stringio/StringIOLibrary.java b/ext/java/org/jruby/ext/stringio/StringIOLibrary.java new file mode 100644 index 0000000..e09f0b0 --- /dev/null +++ b/ext/java/org/jruby/ext/stringio/StringIOLibrary.java @@ -0,0 +1,40 @@ +/***** 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.stringio; + +import java.io.IOException; + +import org.jruby.Ruby; +import org.jruby.runtime.load.Library; + +public class StringIOLibrary implements Library { + public void load(Ruby runtime, boolean wrap) throws IOException { + StringIO.createStringIOClass(runtime); + } +} diff --git a/lib/stringio.rb b/lib/stringio.rb new file mode 100644 index 0000000..10ade45 --- /dev/null +++ b/lib/stringio.rb @@ -0,0 +1,6 @@ +if RUBY_ENGINE == 'jruby' + require 'stringio.jar' + JRuby::Util.load_ext("org.jruby.ext.stringio.StringIOLibrary") +else + require 'stringio.so' +end diff --git a/stringio.gemspec b/stringio.gemspec index 524d976..a429e5c 100644 --- a/stringio.gemspec +++ b/stringio.gemspec @@ -16,11 +16,18 @@ Gem::Specification.new do |s| s.required_rubygems_version = Gem::Requirement.new(">= 2.6") s.require_paths = ["lib"] - s.authors = ["Nobu Nakada"] + s.authors = ["Nobu Nakada", "Charles Oliver Nutter"] s.description = "Pseudo `IO` class from/to `String`." - s.email = "nobu@ruby-lang.org" - s.extensions = ["ext/stringio/extconf.rb"] - s.files = ["README.md", "ext/stringio/extconf.rb", "ext/stringio/stringio.c"] + s.email = ["nobu@ruby-lang.org", "headius@headius.com"] + s.files = ["README.md"] + jruby = true if Gem::Platform.new('java') =~ s.platform or RUBY_ENGINE == 'jruby' + if jruby + s.files += ["lib/stringio.rb", "lib/stringio.jar"] + s.platform = "java" + else + s.extensions = ["ext/stringio/extconf.rb"] + s.files += ["ext/stringio/extconf.rb", "ext/stringio/stringio.c"] + end s.homepage = "https://github.com/ruby/stringio" s.licenses = ["Ruby", "BSD-2-Clause"] s.required_ruby_version = ">= 2.5"