From ba43e15e72779b0a67fd34740b50f033de23fa18 Mon Sep 17 00:00:00 2001 From: James Roseborough Date: Sun, 22 Mar 2015 00:36:44 +0000 Subject: [PATCH] Add ReadOnlyTable and ReadWriteShadowTable, add sandboxing example code, make string metatable a real metatable. --- README.html | 4 + examples/jse/SampleSandboxed.java | 125 +++++ src/core/org/luaj/vm2/Globals.java | 49 +- src/core/org/luaj/vm2/LuaString.java | 9 +- src/core/org/luaj/vm2/ReadOnlyTable.java | 125 +++++ .../org/luaj/vm2/ReadWriteShadowTable.java | 67 +++ src/core/org/luaj/vm2/lib/StringLib.java | 429 +++++++++--------- 7 files changed, 586 insertions(+), 222 deletions(-) create mode 100644 examples/jse/SampleSandboxed.java create mode 100644 src/core/org/luaj/vm2/ReadOnlyTable.java create mode 100644 src/core/org/luaj/vm2/ReadWriteShadowTable.java diff --git a/README.html b/README.html index 0309b4e9..81fcae93 100644 --- a/README.html +++ b/README.html @@ -980,6 +980,10 @@ Files are no longer hosted at LuaForge.
  • Improve garbage collection of orphaned coroutines when yielding from debug hook functions (fixes issue #32).
  • LuaScriptEngineFactory.getScriptEngine() now returns new instance of LuaScriptEngine for each call.
  • Fix os.date("*t") to return hour in 24 hour format (fixes issue #45)
  • +
  • Add ReadOnlyTable and ReadWriteShadowTable utility classes to simplify sandboxing.
  • +
  • Make string metatable a proper metatable, and make it read-only by default.
  • +
  • Add sample code that illustrates techniques in creating sandboxed environments.
  • +
  • Add convenience methods to Global to load string scripts with custom environment.
  • diff --git a/examples/jse/SampleSandboxed.java b/examples/jse/SampleSandboxed.java new file mode 100644 index 00000000..56411dba --- /dev/null +++ b/examples/jse/SampleSandboxed.java @@ -0,0 +1,125 @@ +import org.luaj.vm2.*; +import org.luaj.vm2.compiler.LuaC; +import org.luaj.vm2.lib.*; +import org.luaj.vm2.lib.jse.*; + +/** Simple program that illustrates basic sand-boxing of client scripts + * in a server environment. + * + *

    Although this sandboxing is done primarily in Java here, most of the + * same techniques can be done directly from lua using metatables. + * + *

    This class makes particular use of two utility classes, + * {@link ReadOnlyTable} which is used to wrap shared global metatables + * such as the string metatable or the number metatable, and + * {@link ReadWriteShadowTable} which can provide a lightweight user + * environment around an arbitrarily deep shared globals instance while + * limiting the resources at startup for small scripts that use few globals. + * + * @see Globals + * @see LuaValue + * @see ReadOnlyTable + * @see ReadWriteShadowTable + */ +public class SampleSandboxed { + + // Globals use by the server itself, say to compile scripts that are loaded. + // In a real server there should be one of these per server thread. + // See SampleMultiThreaded.java for an example of multi-threaded setup. + static final Globals server_globals = JsePlatform.debugGlobals(); + + // A set of global functions and packages that are shared across users. + // These are exposed in a read-only fashion through user environment + // shadow tables. + static final Globals shared_globals = new Globals(); + static { + // Load only packages known to be safe for multiple users. + shared_globals.load(new JseBaseLib()); + shared_globals.load(new PackageLib()); + shared_globals.load(new Bit32Lib()); + shared_globals.load(new TableLib()); + shared_globals.load(new StringLib()); + shared_globals.load(new JseMathLib()); + LoadState.install(shared_globals); + LuaC.install(shared_globals); + } + + // Create a new user environment which refers to, but does not modify the + // shared globals. Each top-level user script should get their own copy + // of these which are a lightweight shadow of the shared globals. + // Writes to these shadow tables do not affect the original table. + static LuaTable create_user_environment() { + LuaTable user_environment = new ReadWriteShadowTable(shared_globals); + user_environment.set("_G", user_environment); + return user_environment; + } + + public static void main(String[] args) { + + // Should be able to see and use globals as if they are owned by this environment. + expectSuccess("print('_G', _G)"); + expectSuccess("print('math.pi', math.pi)");; + expectSuccess("x = 'abc'; print('_G.x', _G.x); assert(_G.x == 'abc')"); + expectSuccess("print('_G.x', _G.x); assert(x == nil)"); + + // Should not be able to write to global shared metatables. + expectSuccess("print('string meta', getmetatable('abc'))"); + expectException("print('string meta.x=foo'); getmetatable('abc')['x']='foo'"); + expectException("print('setmetatable(abc)', setmetatable('abc', {}))"); + expectException("print('setmetatable(true)', setmetatable(true, {}))"); + + // Should be able to provide useful global server metatable behavior + // Example use of shared global metatable. + // Allows bools to be added to numbers. + LuaBoolean.s_metatable = new ReadOnlyTable(new LuaValue[] { + LuaValue.ADD, new TwoArgFunction() { + public LuaValue call(LuaValue x, LuaValue y) { + return LuaValue.valueOf( + (x == TRUE ? 1.0 : x.todouble()) + + (y == TRUE ? 1.0 : y.todouble()) ); + } + }, + }); + expectSuccess("print('pi + true', math.pi + true)"); + + // Should be able to use the metatable for our own globals. + expectSuccess("setmetatable(_G, {__index={foo='bar'}}); print('foo', foo); assert(foo == 'bar')"); + + // Subtables of globals can be modified but are shadow tables and don't affect + // shared globals or environment of other scripts. + expectSuccess("print('setmetatable(math)', setmetatable(math, {foo='bar'}))"); + expectSuccess("print('getmetatable(math)', getmetatable(math)); assert(getmetatable(math) == nil)"); + } + + + // Run a script and return the results. + static Varargs runScript(String script) { + LuaTable user_environment = create_user_environment(); + LuaValue chunk = server_globals.load(script, "main", user_environment); + return chunk.invoke(); + } + + // Run a script, expecting it to succeed, to illustrate various uses + // that should succeed without affecting shared resources. + static Varargs expectSuccess(String script) { + try { + return runScript(script); + } catch (Throwable t) { + System.out.println("script failed: "+t); + return LuaValue.NONE; + } + } + + // Run a script, expecting it to fail, to illustrate various expected ways + // rogue attempts will fail to abuse resources. + static Varargs expectException(String script) { + try { + Varargs result = runScript(script); + System.out.println("failure: script returned "+ result); + return result; + } catch (Throwable t) { + System.out.println("success: "+t.getMessage()); + return LuaValue.NONE; + } + } +} diff --git a/src/core/org/luaj/vm2/Globals.java b/src/core/org/luaj/vm2/Globals.java index e048a2c3..68812e59 100644 --- a/src/core/org/luaj/vm2/Globals.java +++ b/src/core/org/luaj/vm2/Globals.java @@ -202,19 +202,55 @@ public class Globals extends LuaTable { public LuaValue load(String script) { return load(new StrReader(script), script); } - + + /** Convenience function to load a string value as a script with a custom environment. + * Must be lua source. + * @param script Contents of a lua script, such as "print 'hello, world.'" + * @param chunkname Name that will be used within the chunk as the source. + * @param environment LuaTable to be used as the environment for the loaded function. + * @return LuaValue that may be executed via .call(), .invoke(), or .method() calls. + * @throws LuaError if the script could not be compiled. + */ + public LuaValue load(String script, String chunkname, LuaTable environment) { + return load(new StrReader(script), chunkname, environment); + } + /** Load the content form a reader as a text file. Must be lua source. * The source is converted to UTF-8, so any characters appearing in quoted literals - * above the range 128 will be converted into multiple bytes. */ + * above the range 128 will be converted into multiple bytes. + * @param script Contents of a lua script, such as "print 'hello, world.'" + * @param chunkname Name that will be used within the chunk as the source. + * @return LuaValue that may be executed via .call(), .invoke(), or .method() calls. + * @throws LuaError if the script could not be compiled. + */ public LuaValue load(Reader reader, String chunkname) { return load(new UTF8Stream(reader), chunkname, "t", this); } - /** Load the content form an input stream as a binary chunk or text file. */ - public LuaValue load(InputStream is, String chunkname, String mode, LuaValue env) { + /** Load the content form a reader as a text file, supplying a custom environment. + * Must be lua source. The source is converted to UTF-8, so any characters + * appearing in quoted literals above the range 128 will be converted into + * multiple bytes. + * @param script Contents of a lua script, such as "print 'hello, world.'" + * @param chunkname Name that will be used within the chunk as the source. + * @param environment LuaTable to be used as the environment for the loaded function. + * @return LuaValue that may be executed via .call(), .invoke(), or .method() calls. + * @throws LuaError if the script could not be compiled. + */ + public LuaValue load(Reader reader, String chunkname, LuaTable environment) { + return load(new UTF8Stream(reader), chunkname, "t", environment); + } + + /** Load the content form an input stream as a binary chunk or text file. + * @param is Input stream containing a lua script or compiled lua" + * @param chunkname Name that will be used within the chunk as the source. + * @param mode String containing 'b' or 't' or both to control loading as binary or text or either. + * @param environment LuaTable to be used as the environment for the loaded function. + * */ + public LuaValue load(InputStream is, String chunkname, String mode, LuaValue environment) { try { Prototype p = loadPrototype(is, chunkname, mode); - return loader.load(p, chunkname, env); + return loader.load(p, chunkname, environment); } catch (LuaError l) { throw l; } catch (Exception e) { @@ -225,6 +261,9 @@ public class Globals extends LuaTable { /** Load lua source or lua binary from an input stream into a Prototype. * The InputStream is either a binary lua chunk starting with the lua binary chunk signature, * or a text input file. If it is a text input file, it is interpreted as a UTF-8 byte sequence. + * @param is Input stream containing a lua script or compiled lua" + * @param chunkname Name that will be used within the chunk as the source. + * @param mode String containing 'b' or 't' or both to control loading as binary or text or either. */ public Prototype loadPrototype(InputStream is, String chunkname, String mode) throws IOException { if (mode.indexOf('b') >= 0) { diff --git a/src/core/org/luaj/vm2/LuaString.java b/src/core/org/luaj/vm2/LuaString.java index f1d73bfc..9da5f1b5 100644 --- a/src/core/org/luaj/vm2/LuaString.java +++ b/src/core/org/luaj/vm2/LuaString.java @@ -62,9 +62,9 @@ import org.luaj.vm2.lib.StringLib; */ public class LuaString extends LuaValue { - /** The singleton instance representing lua {@code true} */ + /** The singleton instance for string metatables that forwards to the string functions */ public static LuaValue s_metatable; - + /** The bytes for the string. These must not be mutated directly because * the backing may be shared by multiple LuaStrings, and the hash code is * computed only at construction time. @@ -254,11 +254,6 @@ public class LuaString extends LuaValue { return decodeAsUtf8(m_bytes, m_offset, m_length); } - // get is delegated to the string library - public LuaValue get(LuaValue key) { - return s_metatable!=null? gettable(this,key): StringLib.instance.get(key); - } - // unary operators public LuaValue neg() { double d = scannumber(); return Double.isNaN(d)? super.neg(): valueOf(-d); } diff --git a/src/core/org/luaj/vm2/ReadOnlyTable.java b/src/core/org/luaj/vm2/ReadOnlyTable.java new file mode 100644 index 00000000..e31d5060 --- /dev/null +++ b/src/core/org/luaj/vm2/ReadOnlyTable.java @@ -0,0 +1,125 @@ +/******************************************************************************* + * Copyright (c) 2015 Luaj.org. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ +package org.luaj.vm2; + +/** Utility class to construct a read-only LuaTable whose initial contents are + * initialized to some set of values and whose contents cannot be changed after + * construction, allowing the table to be shared in a multi-threaded context. + */ +public final class ReadOnlyTable extends LuaTable { + + /** Empty read-only table with no metatable. */ + public static final ReadOnlyTable empty_read_only_table = + new ReadOnlyTable(); + + /** Construct a ReadOnlyTable with a set of named key-value pairs. + * 'named' is a list of LuaValues, with the first being a key, the + * second being the corresponding value, and so on. All key-value + * pairs are copied into the table as part of construction, + * however only a shallow copy is done so values which are tables + * remain read-write even if accessed from this read-only table. + * @param named array of values in key,value,key,value order + * which are the key-value pairs that will appear in this read-only table. + */ + public ReadOnlyTable(LuaValue[] named) { + presize(named.length/2, 0); + for ( int i=0; iThis simplifies creation of a safe unique environment for user scripts + * which falls back to a delegate shared globals table. + */ +public class ReadWriteShadowTable extends LuaTable { + /** The underlying table from which values are read when + * the table does not contain a key. The values in delegate + * should not be modified as a result of being a delegate for + * this table, however if the values are userdata that expose + * mutators, those values may undergo mutations once exposed. + */ + public final LuaValue delegate; + + /** Construct a read-write shadow table around 'delegate' without + * copying elements, but retaining a reference to delegate. + * @param fallback The table containing values we would like to + * reference without affecting the contents of that table. + */ + public ReadWriteShadowTable(LuaValue delegate) { + this.delegate = delegate; + } + + /** Normal table get, but return delegate value if not found. + * If the delegate returns a table, wraps that in a read-write shadow + * and does a local rawset on that value before returning it. + * @param key LuaValue to look up. + */ + public LuaValue get(LuaValue key) { + LuaValue value = super.get(key); + if (value.isnil()) { + value = delegate.get(key); + if (value.istable()) + value = new ReadWriteShadowTable(value); + rawset(key, value); + } + return value; + } +} \ No newline at end of file diff --git a/src/core/org/luaj/vm2/lib/StringLib.java b/src/core/org/luaj/vm2/lib/StringLib.java index 80f47377..62553dbb 100644 --- a/src/core/org/luaj/vm2/lib/StringLib.java +++ b/src/core/org/luaj/vm2/lib/StringLib.java @@ -29,6 +29,7 @@ import org.luaj.vm2.Buffer; import org.luaj.vm2.LuaString; import org.luaj.vm2.LuaTable; import org.luaj.vm2.LuaValue; +import org.luaj.vm2.ReadOnlyTable; import org.luaj.vm2.Varargs; import org.luaj.vm2.compiler.DumpState; @@ -61,57 +62,37 @@ import org.luaj.vm2.compiler.DumpState; */ public class StringLib extends TwoArgFunction { - public static LuaTable instance; + public static final ReadOnlyTable lib_functions; + static { + LuaTable t = new LuaTable(); + t.set("byte", new byte_()); + t.set("char", new char_()); + t.set("dump", new dump()); + t.set("find", new find()); + t.set("format", new format()); + t.set("gmatch", new gmatch()); + t.set("gsub", new gsub()); + t.set("len", new len()); + t.set("lower", new lower()); + t.set("match", new match()); + t.set("rep", new rep()); + t.set("reverse", new reverse()); + t.set("sub", new sub()); + t.set("upper", new upper()); + lib_functions = new ReadOnlyTable(t); + LuaString.s_metatable = new ReadOnlyTable( + new LuaValue[] { INDEX, lib_functions }); + } public StringLib() { } public LuaValue call(LuaValue modname, LuaValue env) { - LuaTable t = new LuaTable(); - bind(t, StringLib1.class, new String[] { - "dump", "len", "lower", "reverse", "upper", } ); - bind(t, StringLibV.class, new String[] { - "byte", "char", "find", "format", - "gmatch", "gsub", "match", "rep", - "sub"} ); - env.set("string", t); - instance = t; - if ( LuaString.s_metatable == null ) - LuaString.s_metatable = tableOf( new LuaValue[] { INDEX, t } ); - env.get("package").get("loaded").set("string", t); - return t; + env.set("string", lib_functions); + env.get("package").get("loaded").set("string", lib_functions); + return lib_functions; } - static final class StringLib1 extends OneArgFunction { - public LuaValue call(LuaValue arg) { - switch ( opcode ) { - case 0: return dump(arg); // dump (function) - case 1: return StringLib.len(arg); // len (function) - case 2: return lower(arg); // lower (function) - case 3: return reverse(arg); // reverse (function) - case 4: return upper(arg); // upper (function) - } - return NIL; - } - } - - static final class StringLibV extends VarArgFunction { - public Varargs invoke(Varargs args) { - switch ( opcode ) { - case 0: return StringLib.byte_( args ); - case 1: return StringLib.char_( args ); - case 2: return StringLib.find( args ); - case 3: return StringLib.format( args ); - case 4: return StringLib.gmatch( args ); - case 5: return StringLib.gsub( args ); - case 6: return StringLib.match( args ); - case 7: return StringLib.rep( args ); - case 8: return StringLib.sub( args ); - } - return NONE; - } - } - /** * string.byte (s [, i [, j]]) * @@ -123,22 +104,24 @@ public class StringLib extends TwoArgFunction { * * @param args the calling args */ - static Varargs byte_( Varargs args ) { - LuaString s = args.checkstring(1); - int l = s.m_length; - int posi = posrelat( args.optint(2,1), l ); - int pose = posrelat( args.optint(3,posi), l ); - int n,i; - if (posi <= 0) posi = 1; - if (pose > l) pose = l; - if (posi > pose) return NONE; /* empty interval; return no values */ - n = (int)(pose - posi + 1); - if (posi + n <= pose) /* overflow? */ - error("string slice too long"); - LuaValue[] v = new LuaValue[n]; - for (i=0; i l) pose = l; + if (posi > pose) return NONE; /* empty interval; return no values */ + n = (int)(pose - posi + 1); + if (posi + n <= pose) /* overflow? */ + error("string slice too long"); + LuaValue[] v = new LuaValue[n]; + for (i=0; i=256) argerror(a, "invalid value"); - bytes[i] = (byte) c; + static final class char_ extends VarArgFunction { + public Varargs invoke(Varargs args) { + int n = args.narg(); + byte[] bytes = new byte[n]; + for ( int i=0, a=1; i=256) argerror(a, "invalid value"); + bytes[i] = (byte) c; + } + return LuaString.valueUsing( bytes ); } - return LuaString.valueUsing( bytes ); } /** @@ -172,14 +157,16 @@ public class StringLib extends TwoArgFunction { * * TODO: port dumping code as optional add-on */ - static LuaValue dump( LuaValue arg ) { - LuaValue f = arg.checkfunction(); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try { - DumpState.dump( ((LuaClosure)f).p, baos, true ); - return LuaString.valueUsing(baos.toByteArray()); - } catch (IOException e) { - return error( e.getMessage() ); + static final class dump extends OneArgFunction { + public LuaValue call(LuaValue arg) { + LuaValue f = arg.checkfunction(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + DumpState.dump( ((LuaClosure)f).p, baos, true ); + return LuaString.valueUsing(baos.toByteArray()); + } catch (IOException e) { + return error( e.getMessage() ); + } } } @@ -199,8 +186,10 @@ public class StringLib extends TwoArgFunction { * If the pattern has captures, then in a successful match the captured values * are also returned, after the two indices. */ - static Varargs find( Varargs args ) { - return str_find_aux( args, true ); + static final class find extends VarArgFunction { + public Varargs invoke(Varargs args) { + return str_find_aux( args, true ); + } } /** @@ -226,72 +215,74 @@ public class StringLib extends TwoArgFunction { * This function does not accept string values containing embedded zeros, * except as arguments to the q option. */ - static Varargs format( Varargs args ) { - LuaString fmt = args.checkstring( 1 ); - final int n = fmt.length(); - Buffer result = new Buffer(n); - int arg = 1; - int c; - - for ( int i = 0; i < n; ) { - switch ( c = fmt.luaByte( i++ ) ) { - case '\n': - result.append( "\n" ); - break; - default: - result.append( (byte) c ); - break; - case L_ESC: - if ( i < n ) { - if ( ( c = fmt.luaByte( i ) ) == L_ESC ) { - ++i; - result.append( (byte)L_ESC ); - } else { - arg++; - FormatDesc fdsc = new FormatDesc(args, fmt, i ); - i += fdsc.length; - switch ( fdsc.conversion ) { - case 'c': - fdsc.format( result, (byte)args.checkint( arg ) ); - break; - case 'i': - case 'd': - fdsc.format( result, args.checkint( arg ) ); - break; - case 'o': - case 'u': - case 'x': - case 'X': - fdsc.format( result, args.checklong( arg ) ); - break; - case 'e': - case 'E': - case 'f': - case 'g': - case 'G': - fdsc.format( result, args.checkdouble( arg ) ); - break; - case 'q': - addquoted( result, args.checkstring( arg ) ); - break; - case 's': { - LuaString s = args.checkstring( arg ); - if ( fdsc.precision == -1 && s.length() >= 100 ) { - result.append( s ); - } else { - fdsc.format( result, s ); + static final class format extends VarArgFunction { + public Varargs invoke(Varargs args) { + LuaString fmt = args.checkstring( 1 ); + final int n = fmt.length(); + Buffer result = new Buffer(n); + int arg = 1; + int c; + + for ( int i = 0; i < n; ) { + switch ( c = fmt.luaByte( i++ ) ) { + case '\n': + result.append( "\n" ); + break; + default: + result.append( (byte) c ); + break; + case L_ESC: + if ( i < n ) { + if ( ( c = fmt.luaByte( i ) ) == L_ESC ) { + ++i; + result.append( (byte)L_ESC ); + } else { + arg++; + FormatDesc fdsc = new FormatDesc(args, fmt, i ); + i += fdsc.length; + switch ( fdsc.conversion ) { + case 'c': + fdsc.format( result, (byte)args.checkint( arg ) ); + break; + case 'i': + case 'd': + fdsc.format( result, args.checkint( arg ) ); + break; + case 'o': + case 'u': + case 'x': + case 'X': + fdsc.format( result, args.checklong( arg ) ); + break; + case 'e': + case 'E': + case 'f': + case 'g': + case 'G': + fdsc.format( result, args.checkdouble( arg ) ); + break; + case 'q': + addquoted( result, args.checkstring( arg ) ); + break; + case 's': { + LuaString s = args.checkstring( arg ); + if ( fdsc.precision == -1 && s.length() >= 100 ) { + result.append( s ); + } else { + fdsc.format( result, s ); + } + } break; + default: + error("invalid option '%"+(char)fdsc.conversion+"' to 'format'"); + break; } - } break; - default: - error("invalid option '%"+(char)fdsc.conversion+"' to 'format'"); - break; } } } } + + return result.tostring(); } - - return result.tostring(); } private static void addquoted(Buffer buf, LuaString s) { @@ -503,10 +494,12 @@ public class StringLib extends TwoArgFunction { * For this function, a '^' at the start of a pattern does not work as an anchor, * as this would prevent the iteration. */ - static Varargs gmatch( Varargs args ) { - LuaString src = args.checkstring( 1 ); - LuaString pat = args.checkstring( 2 ); - return new GMatchAux(args, src, pat); + static final class gmatch extends VarArgFunction { + public Varargs invoke(Varargs args) { + LuaString src = args.checkstring( 1 ); + LuaString pat = args.checkstring( 2 ); + return new GMatchAux(args, src, pat); + } } static class GMatchAux extends VarArgFunction { @@ -578,37 +571,39 @@ public class StringLib extends TwoArgFunction { * x = string.gsub("$name-$version.tar.gz", "%$(%w+)", t) * --> x="lua-5.1.tar.gz" */ - static Varargs gsub( Varargs args ) { - LuaString src = args.checkstring( 1 ); - final int srclen = src.length(); - LuaString p = args.checkstring( 2 ); - LuaValue repl = args.arg( 3 ); - int max_s = args.optint( 4, srclen + 1 ); - final boolean anchor = p.length() > 0 && p.charAt( 0 ) == '^'; - - Buffer lbuf = new Buffer( srclen ); - MatchState ms = new MatchState( args, src, p ); - - int soffset = 0; - int n = 0; - while ( n < max_s ) { - ms.reset(); - int res = ms.match( soffset, anchor ? 1 : 0 ); - if ( res != -1 ) { - n++; - ms.add_value( lbuf, soffset, res, repl ); + static final class gsub extends VarArgFunction { + public Varargs invoke(Varargs args) { + LuaString src = args.checkstring( 1 ); + final int srclen = src.length(); + LuaString p = args.checkstring( 2 ); + LuaValue repl = args.arg( 3 ); + int max_s = args.optint( 4, srclen + 1 ); + final boolean anchor = p.length() > 0 && p.charAt( 0 ) == '^'; + + Buffer lbuf = new Buffer( srclen ); + MatchState ms = new MatchState( args, src, p ); + + int soffset = 0; + int n = 0; + while ( n < max_s ) { + ms.reset(); + int res = ms.match( soffset, anchor ? 1 : 0 ); + if ( res != -1 ) { + n++; + ms.add_value( lbuf, soffset, res, repl ); + } + if ( res != -1 && res > soffset ) + soffset = res; + else if ( soffset < srclen ) + lbuf.append( (byte) src.luaByte( soffset++ ) ); + else + break; + if ( anchor ) + break; } - if ( res != -1 && res > soffset ) - soffset = res; - else if ( soffset < srclen ) - lbuf.append( (byte) src.luaByte( soffset++ ) ); - else - break; - if ( anchor ) - break; + lbuf.append( src.substring( soffset, srclen ) ); + return varargsOf(lbuf.tostring(), valueOf(n)); } - lbuf.append( src.substring( soffset, srclen ) ); - return varargsOf(lbuf.tostring(), valueOf(n)); } /** @@ -617,8 +612,10 @@ public class StringLib extends TwoArgFunction { * Receives a string and returns its length. The empty string "" has length 0. * Embedded zeros are counted, so "a\000bc\000" has length 5. */ - static LuaValue len( LuaValue arg ) { - return arg.checkstring().len(); + static final class len extends OneArgFunction { + public LuaValue call(LuaValue arg) { + return arg.checkstring().len(); + } } /** @@ -628,8 +625,10 @@ public class StringLib extends TwoArgFunction { * changed to lowercase. All other characters are left unchanged. * The definition of what an uppercase letter is depends on the current locale. */ - static LuaValue lower( LuaValue arg ) { - return valueOf( arg.checkjstring().toLowerCase() ); + static final class lower extends OneArgFunction { + public LuaValue call(LuaValue arg) { + return valueOf( arg.checkjstring().toLowerCase() ); + } } /** @@ -641,8 +640,10 @@ public class StringLib extends TwoArgFunction { * A third, optional numerical argument init specifies where to start the * search; its default value is 1 and may be negative. */ - static Varargs match( Varargs args ) { - return str_find_aux( args, false ); + static final class match extends VarArgFunction { + public Varargs invoke(Varargs args) { + return str_find_aux( args, false ); + } } /** @@ -650,15 +651,17 @@ public class StringLib extends TwoArgFunction { * * Returns a string that is the concatenation of n copies of the string s. */ - static Varargs rep( Varargs args ) { - LuaString s = args.checkstring( 1 ); - int n = args.checkint( 2 ); - final byte[] bytes = new byte[ s.length() * n ]; - int len = s.length(); - for ( int offset = 0; offset < bytes.length; offset += len ) { - s.copyInto( 0, bytes, offset, len ); + static final class rep extends VarArgFunction { + public Varargs invoke(Varargs args) { + LuaString s = args.checkstring( 1 ); + int n = args.checkint( 2 ); + final byte[] bytes = new byte[ s.length() * n ]; + int len = s.length(); + for ( int offset = 0; offset < bytes.length; offset += len ) { + s.copyInto( 0, bytes, offset, len ); + } + return LuaString.valueUsing( bytes ); } - return LuaString.valueUsing( bytes ); } /** @@ -666,13 +669,15 @@ public class StringLib extends TwoArgFunction { * * Returns a string that is the string s reversed. */ - static LuaValue reverse( LuaValue arg ) { - LuaString s = arg.checkstring(); - int n = s.length(); - byte[] b = new byte[n]; - for ( int i=0, j=n-1; i l ) - end = l; - - if ( start <= end ) { - return s.substring( start-1 , end ); - } else { - return EMPTYSTRING; + static final class sub extends VarArgFunction { + public Varargs invoke(Varargs args) { + final LuaString s = args.checkstring( 1 ); + final int l = s.length(); + + int start = posrelat( args.checkint( 2 ), l ); + int end = posrelat( args.optint( 3, -1 ), l ); + + if ( start < 1 ) + start = 1; + if ( end > l ) + end = l; + + if ( start <= end ) { + return s.substring( start-1 , end ); + } else { + return EMPTYSTRING; + } } } @@ -712,8 +719,10 @@ public class StringLib extends TwoArgFunction { * changed to uppercase. All other characters are left unchanged. * The definition of what a lowercase letter is depends on the current locale. */ - static LuaValue upper( LuaValue arg ) { - return valueOf(arg.checkjstring().toUpperCase()); + static final class upper extends OneArgFunction { + public LuaValue call(LuaValue arg) { + return valueOf(arg.checkjstring().toUpperCase()); + } } /**