Index: src/builtin/strscan.rb
===================================================================
--- src/builtin/strscan.rb	(revision 2110)
+++ src/builtin/strscan.rb	(working copy)
@@ -1,97 +0,0 @@
-# Slow pure-ruby version of strscan
-# This is missing many strscan functions.  The only implemented methods are the ones which
-# erb uses.
-
-class StringScanner
-  attr_reader :matched, :pre_match, :post_match
-  
-  def initialize(string)
-    @string = string
-    @position = 0
-  end
-  
-  def check(regexp)
-    sub_string = @string.slice(@position..-1)
-    regexp = Regexp.new(regexp) unless regexp.kind_of? Regexp
-    regexp = Regexp.new("^" + regexp.source)
-    @match_data = regexp.match(sub_string)
-    if @match_data.nil?
-      @pre_match, @matched, @post_match = nil, nil, nil
-	  return nil 
-    end
-	@pre_match, @matched, @post_match = @match_data.pre_match, @match_data[0], @match_data.post_match
-    @match_data[0]
-  end
-  
-  def getch
-    a = @string[@position..@position]
-    @position = @position + 1 unless eos?
-    @matched = a == "" ? nil : a
-    @pre_match = @string.slice(0, @position-1)
-    @matched
-  end
-  
-  def pos
-    @position
-  end
-  
-  def pos=(position)
-    @position = position
-  end
-  
-  def rest
-  	@string.slice(@position..-1)
-  end
-
-  def scan(regexp)
-    sub_string = @string.slice(@position..-1)
-    regexp = Regexp.new(regexp) unless regexp.kind_of? Regexp
-    regexp = Regexp.new("^" + regexp.source)
-    @match_data = regexp.match(sub_string)
-    if @match_data.nil?
-      @pre_match, @matched, @post_match = nil, nil, nil
-      return nil 
-    end
-    
-	@pre_match, @matched, @post_match = @match_data.pre_match, @match_data[0], @match_data.post_match
-    @position = @position + @match_data.end(0)
-    @matched
-  end
-  
-  def scan_until(regexp)
-    sub_string = @string.slice(@position..-1)
-    regexp = Regexp.new(regexp) unless regexp.kind_of? Regexp
-    @match_data = regexp.match(sub_string)
-    if @match_data.nil?
-      @pre_match, @matched, @post_match = nil, nil, nil
-	  return nil 
-	end
-	
-	@pre_match, @matched, @post_match = @match_data.pre_match, @match_data[0], @match_data.post_match
-    @position = @position + @match_data.end(0)
-	@pre_match + @matched
-  end
-  
-  def skip(regexp)
-    sub_string = @string.slice(@position..-1)
-    regexp = Regexp.new(regexp) unless regexp.kind_of? Regexp
-    regexp = Regexp.new("^" + regexp.source)
-    @match_data = regexp.match(sub_string)
-    if @match_data.nil?
-      @pre_match, @matched, @post_match = nil, nil, nil
-      return nil
-    end
-    
-	@pre_match, @matched, @post_match = @match_data.pre_match, @match_data[0], @match_data.post_match
-    @position = @position + @match_data.end(0)
-    @match_data.end(0)
-  end
-
-  def [](i)
-    @match_data[i]
-  end
-  
-  def eos?
-    @position >= @string.length
-  end
-end
Index: src/org/jruby/Ruby.java
===================================================================
--- src/org/jruby/Ruby.java	(revision 2110)
+++ src/org/jruby/Ruby.java	(working copy)
@@ -61,6 +61,7 @@
 import org.jruby.libraries.RbConfigLibrary;
 import org.jruby.libraries.SocketLibrary;
 import org.jruby.libraries.StringIOLibrary;
+import org.jruby.libraries.StringScannerLibrary;
 import org.jruby.libraries.ZlibLibrary;
 import org.jruby.parser.Parser;
 import org.jruby.runtime.Block;
@@ -98,7 +99,7 @@
  * The jruby runtime.
  */
 public final class Ruby implements IRuby {
-	private static String[] BUILTIN_LIBRARIES = {"fcntl", "yaml", "etc", "nkf", "strscan"};
+	private static String[] BUILTIN_LIBRARIES = {"fcntl", "yaml", "etc", "nkf" };
     
 	private CacheMap cacheMap = new CacheMap();
     private ThreadService threadService = new ThreadService(this);
@@ -426,6 +427,7 @@
         
         loadService.registerBuiltin("jruby.rb", new JRubyLibrary());
         loadService.registerBuiltin("stringio.rb", new StringIOLibrary());
+        loadService.registerBuiltin("strscan.rb", new StringScannerLibrary());
         loadService.registerBuiltin("zlib.rb", new ZlibLibrary());
     }
 
Index: src/org/jruby/RubyRegexp.java
===================================================================
--- src/org/jruby/RubyRegexp.java	(revision 2110)
+++ src/org/jruby/RubyRegexp.java	(working copy)
@@ -644,4 +644,8 @@
         }
         output.dumpInt(flags);
     }
+	
+	public Pattern getPattern() {
+		return this.pattern;
+	}
 }
Index: src/org/jruby/RubyString.java
===================================================================
--- src/org/jruby/RubyString.java	(revision 2110)
+++ src/org/jruby/RubyString.java	(working copy)
@@ -193,7 +193,7 @@
 	/** rb_str_new2
 	 *
 	 */
-	public static RubyString newString(IRuby runtime, String str) {
+	public static RubyString newString(IRuby runtime, CharSequence str) {
 		return new RubyString(runtime, str);
 	}
 
Index: src/org/jruby/libraries/StringScannerLibrary.java
===================================================================
--- src/org/jruby/libraries/StringScannerLibrary.java	
+++ src/org/jruby/libraries/StringScannerLibrary.java	
@@ -0,0 +1,22 @@
+package org.jruby.libraries;
+
+import java.io.IOException;
+
+import org.jruby.IRuby;
+import org.jruby.RubyStringScanner;
+import org.jruby.runtime.load.Library;
+
+/**
+ * @author kscott
+ *
+ */
+public class StringScannerLibrary implements Library {
+
+	/**
+	 * @see org.jruby.runtime.load.Library#load(org.jruby.IRuby)
+	 */
+	public void load(IRuby runtime) throws IOException {
+		RubyStringScanner.createScannerClass(runtime);
+	}
+
+}
Index: src/org/jruby/runtime/CallbackFactory.java
===================================================================
--- src/org/jruby/runtime/CallbackFactory.java	(revision 2110)
+++ src/org/jruby/runtime/CallbackFactory.java	(working copy)
@@ -66,6 +66,16 @@
      * @return a CallBack object corresponding to the appropriate method
      **/
     public abstract Callback getMethod(String method, Class arg1, Class arg2);
+    
+    /**
+     * gets an instance method with two arguments.
+     * @param method name of the method
+     * @param arg1 the java class of the first argument for this method
+     * @param arg2 the java class of the second argument for this method
+     * @param arg3 the java class of the second argument for this method
+     * @return a CallBack object corresponding to the appropriate method
+     **/
+    public abstract Callback getMethod(String method, Class arg1, Class arg2, Class arg3);
 
     /**
      * gets a singleton (class) method without arguments.
Index: src/org/jruby/runtime/callback/ReflectionCallbackFactory.java
===================================================================
--- src/org/jruby/runtime/callback/ReflectionCallbackFactory.java	(revision 2110)
+++ src/org/jruby/runtime/callback/ReflectionCallbackFactory.java	(working copy)
@@ -51,6 +51,10 @@
     public Callback getMethod(String method, Class arg1, Class arg2) {
         return new ReflectionCallback(type, method, new Class[] { arg1, arg2 }, false, false, Arity.fixed(2));
     }
+    
+    public Callback getMethod(String method, Class arg1, Class arg2, Class arg3) {
+        return new ReflectionCallback(type, method, new Class[] { arg1, arg2, arg3 }, false, false, Arity.fixed(3));
+    }
 
     public Callback getSingletonMethod(String method) {
         return new ReflectionCallback(type, method, NULL_CLASS_ARRAY, false, true, Arity.noArguments());
Index: src/org/jruby/util/StringScanner.java
===================================================================
--- src/org/jruby/util/StringScanner.java	
+++ src/org/jruby/util/StringScanner.java	
@@ -0,0 +1,297 @@
+package org.jruby.util;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @author kscott
+ *
+ */
+public class StringScanner {
+	
+	private CharSequence string;
+	private Matcher matcher;
+	private int pos = 0;
+	private int lastPos = -1;
+	private int matchStart = -1;
+	private int matchEnd = -1;
+
+	/**
+	 * 
+	 */
+	public StringScanner() {
+		this("");
+	}
+	
+	public StringScanner(CharSequence string) {
+		this.string = string;
+	}
+	
+	public boolean isEndOfString() {
+		return pos == string.length();
+	}
+	
+	public boolean isBeginningOfLine() {
+		return pos == 0 || string.charAt(pos - 1) == '\n';
+	}
+	
+	public CharSequence getString() {
+		return string;
+	}
+	
+	private void resetMatchData() {
+		matcher = null;
+		matchStart = -1;
+		matchEnd = -1;
+	}
+
+	public void terminate() {
+		pos = string.length();
+		lastPos = -1;
+		resetMatchData();
+	}
+	
+	public void reset() {
+		pos = 0;
+		lastPos = -1;
+		resetMatchData();
+	}
+	
+	public void setString(CharSequence string) {
+		this.string = string;
+		reset();
+	}
+	
+	public void append(CharSequence string) {
+		StringBuffer buf = new StringBuffer();
+		// JDK 1.4 Doesn't have a constructor that takes a CharSequence
+		buf.append(this.string);
+		buf.append(string);
+		this.string = buf.toString();
+	}
+	
+	public CharSequence rest() {
+		return string.subSequence(pos, string.length());
+	}
+	
+	public int getPos() {
+		return pos;
+	}
+	
+	public void setPos(int pos) {
+		if (pos > string.length()) {
+			throw new IllegalArgumentException("index out of range.");
+		}
+		this.pos = pos;
+	}
+	
+	public char getChar() {
+		if (isEndOfString()) {
+			return 0;
+		} else {
+			matcher = null;
+			matchStart = pos;
+			matchEnd = pos + 1;
+			lastPos = pos;
+			return string.charAt(pos++);
+		}
+	}
+	
+	public boolean matched() {
+		return matchStart > -1;
+	}
+	
+	public CharSequence group(int n) {
+		if (!matched()) {
+			return null;
+		}
+		if (matcher == null && matchEnd - matchStart == 1) {
+			// Handle the getChar() is a match case
+			return string.subSequence(matchStart, matchEnd);
+		}
+		if (n > matcher.groupCount()) {
+			return null;
+		}
+		return matcher.group(n);
+	}
+	
+	public CharSequence preMatch() {
+		if (matched()) {
+			return string.subSequence(0, matchStart);
+		} else {
+			return null;
+		}
+	}
+	
+	public CharSequence postMatch() {
+		if (matched()) {
+			return string.subSequence(matchEnd, string.length());
+		} else {
+			return null;
+		}
+	}
+	
+	public CharSequence matchedValue() {
+		if (matched()) {
+			return string.subSequence(matchStart, matchEnd);
+		} else {
+			return null;
+		}
+	}
+	
+	public int matchedSize() {
+		if (matcher == null) {
+			return -1;
+		} else {
+			return matchEnd - matchStart;
+		}
+	}
+	
+	public void unscan() {
+		if (lastPos != -1) {
+			pos = lastPos;
+			resetMatchData();
+		} else {
+			throw new IllegalStateException("unscan() cannot be called after an unmached scan.");
+		}
+	}
+	
+	public int matches(Pattern pattern) {
+		if (!isEndOfString()) {
+			matcher = pattern.matcher(string.subSequence(pos, string.length()));
+			if (matcher.lookingAt()) {
+				matchStart = pos;
+				matchEnd = matcher.end();
+				return matchEnd;
+			} else {
+				resetMatchData();
+			}
+		}
+		
+		return -1;
+	}
+	
+	public CharSequence scanUntil(Pattern pattern) {
+		if (!isEndOfString()) {
+			matcher = pattern.matcher(string);
+			if (matcher.find(pos)) {
+				lastPos = pos;
+				matchStart = matcher.start();
+				matchEnd = matcher.end();
+				pos = matchEnd;
+				return string.subSequence(lastPos, pos);
+			} else {
+				lastPos = -1;
+				resetMatchData();
+			}
+		}
+		
+		return null;
+	}
+	
+	public CharSequence scan(Pattern pattern) {
+		if (!isEndOfString()) {
+			matcher = pattern.matcher(string.subSequence(pos, string.length()));
+			if (matcher.lookingAt()) {
+				lastPos = pos;
+				matchStart = pos;
+				pos += matcher.end();
+				matchEnd = pos;
+				return matcher.group();
+			} else {
+				lastPos = -1;
+				resetMatchData();
+			}
+		}
+		
+		return null;
+	}
+	
+	public CharSequence check(Pattern pattern) {
+		if (!isEndOfString()) {
+			matcher = pattern.matcher(string.subSequence(pos, string.length()));
+			if (matcher.lookingAt()) {
+				matchStart = pos;
+				matchEnd = matchStart + matcher.end();
+				return matcher.group();
+			} else {
+				resetMatchData();
+			}
+		}
+		
+		return null;
+	}
+	
+	public CharSequence checkUntil(Pattern pattern) {
+		if (!isEndOfString()) {
+			matcher = pattern.matcher(string);
+			if (matcher.find(pos)) {
+				matchStart = matcher.start();
+				matchEnd = matcher.end();
+				return string.subSequence(pos, matcher.end());
+			} else {
+				resetMatchData();
+			}
+		}
+		
+		return null;
+	}
+	
+	public int skip(Pattern pattern) {
+		if (!isEndOfString()) {
+			matcher = pattern.matcher(string.subSequence(pos, string.length()));
+			if (matcher.lookingAt()) {
+				lastPos = pos;
+				matchStart = pos;
+				int end = matcher.end();
+				pos += end;
+				matchEnd = pos;
+				return end;
+			} else {
+				resetMatchData();
+			}
+		}
+		
+		return -1;
+	}
+	
+	public int skipUntil(Pattern pattern) {
+		if (!isEndOfString()) {
+			matcher = pattern.matcher(string);
+			if (matcher.find(pos)) {
+				lastPos = pos;
+				pos = matcher.end();
+				matchStart = matcher.start();
+				matchEnd = pos;
+				return pos - lastPos;
+			} else {
+				resetMatchData();
+			}
+		}
+		
+		return -1;
+	}
+	
+	public int exists(Pattern pattern) {
+		if (!isEndOfString()) {
+			matcher = pattern.matcher(string);
+			if (matcher.find(pos)) {
+				matchStart = matcher.start();
+				matchEnd = matcher.end();
+				return matchEnd - pos;
+			} else {
+				resetMatchData();
+			}
+		}
+		
+		return -1;
+	}
+	
+	public CharSequence peek(int length) {
+		int end = pos + length;
+		if (end > string.length()) {
+			end = string.length(); 
+		}
+		return string.subSequence(pos, end);
+	}
+}
\ No newline at end of file
Index: src/org/jruby/RubyStringScanner.java
===================================================================
--- src/org/jruby/RubyStringScanner.java	
+++ src/org/jruby/RubyStringScanner.java	
@@ -0,0 +1,277 @@
+package org.jruby;
+
+import org.jruby.runtime.CallbackFactory;
+import org.jruby.runtime.builtin.IRubyObject;
+import org.jruby.util.StringScanner;
+
+/**
+ * @author kscott
+ *
+ */
+public class RubyStringScanner extends RubyObject {
+	
+	private StringScanner scanner;
+	
+	public static RubyClass createScannerClass(final IRuby runtime) {
+		RubyClass scannerClass = runtime.defineClass("StringScanner",runtime.getObject());
+		CallbackFactory callbackFactory = runtime.callbackFactory(RubyStringScanner.class);
+		
+		scannerClass.defineSingletonMethod("new", callbackFactory.getOptSingletonMethod("newInstance"));
+		scannerClass.defineMethod("initialize", callbackFactory.getOptMethod("initialize"));
+		scannerClass.defineMethod("<<", callbackFactory.getMethod("concat", IRubyObject.class));
+		scannerClass.defineMethod("concat", callbackFactory.getMethod("concat", IRubyObject.class));
+		scannerClass.defineMethod("[]", callbackFactory.getMethod("group", RubyFixnum.class));
+		scannerClass.defineMethod("beginning_of_line?", callbackFactory.getMethod("bol_p"));
+		scannerClass.defineMethod("bol?", callbackFactory.getMethod("bol_p"));
+		scannerClass.defineMethod("check", callbackFactory.getMethod("check", RubyRegexp.class));
+		scannerClass.defineMethod("check_until", callbackFactory.getMethod("check_until", RubyRegexp.class));
+		scannerClass.defineMethod("clear", callbackFactory.getMethod("terminate"));
+		scannerClass.defineMethod("empty?", callbackFactory.getMethod("eos_p"));
+		scannerClass.defineMethod("eos?", callbackFactory.getMethod("eos_p"));
+		scannerClass.defineMethod("exist?", callbackFactory.getMethod("exist_p", RubyRegexp.class));
+		scannerClass.defineMethod("get_byte", callbackFactory.getMethod("getch"));
+		scannerClass.defineMethod("getbyte", callbackFactory.getMethod("getch"));
+		scannerClass.defineMethod("getch", callbackFactory.getMethod("getch"));
+		scannerClass.defineMethod("inspect", callbackFactory.getMethod("inspect"));
+		scannerClass.defineMethod("match?", callbackFactory.getMethod("match_p", RubyRegexp.class));
+		scannerClass.defineMethod("matched", callbackFactory.getMethod("matched"));
+		scannerClass.defineMethod("matched?", callbackFactory.getMethod("matched_p"));
+		scannerClass.defineMethod("matched_size", callbackFactory.getMethod("matched_size"));
+		scannerClass.defineMethod("matchedsize", callbackFactory.getMethod("matched_size"));
+		scannerClass.defineMethod("peek", callbackFactory.getMethod("peek", RubyFixnum.class));
+		scannerClass.defineMethod("peep", callbackFactory.getMethod("peek", RubyFixnum.class));
+		scannerClass.defineMethod("pointer", callbackFactory.getMethod("pos"));
+		scannerClass.defineMethod("pointer=", callbackFactory.getMethod("set_pos", RubyFixnum.class));
+		scannerClass.defineMethod("pos=", callbackFactory.getMethod("set_pos", RubyFixnum.class));
+		scannerClass.defineMethod("pos", callbackFactory.getMethod("pos"));
+		scannerClass.defineMethod("post_match", callbackFactory.getMethod("post_match"));
+		scannerClass.defineMethod("pre_match", callbackFactory.getMethod("pre_match"));
+		scannerClass.defineMethod("reset", callbackFactory.getMethod("reset"));
+		scannerClass.defineMethod("rest", callbackFactory.getMethod("rest"));
+		scannerClass.defineMethod("rest?", callbackFactory.getMethod("rest_p"));
+		scannerClass.defineMethod("rest_size", callbackFactory.getMethod("rest_size"));
+		scannerClass.defineMethod("restsize", callbackFactory.getMethod("rest_size"));
+		scannerClass.defineMethod("scan", callbackFactory.getMethod("scan", RubyRegexp.class));
+		scannerClass.defineMethod("scan_full", callbackFactory.getMethod("scan_full", RubyRegexp.class, RubyBoolean.class, RubyBoolean.class));
+		scannerClass.defineMethod("scan_until", callbackFactory.getMethod("scan_until", RubyRegexp.class));
+		scannerClass.defineMethod("search_full", callbackFactory.getMethod("search_full", RubyRegexp.class, RubyBoolean.class, RubyBoolean.class));
+		scannerClass.defineMethod("skip", callbackFactory.getMethod("skip", RubyRegexp.class));
+		scannerClass.defineMethod("skip_until", callbackFactory.getMethod("skip_until", RubyRegexp.class));
+		scannerClass.defineMethod("string", callbackFactory.getMethod("string"));
+		scannerClass.defineMethod("string=", callbackFactory.getMethod("set_string", RubyString.class));
+		scannerClass.defineMethod("terminate", callbackFactory.getMethod("terminate"));
+		scannerClass.defineMethod("unscan", callbackFactory.getMethod("unscan"));
+		
+		return scannerClass;
+	}
+	
+	public static IRubyObject newInstance(IRubyObject recv, IRubyObject[] args) {
+        RubyStringScanner result = new RubyStringScanner(recv.getRuntime());
+        result.callInit(args);
+        return result;
+    }
+	
+	protected RubyStringScanner(IRuby runtime) {
+		super(runtime, runtime.getClass("StringScanner"));
+	}
+	
+	public IRubyObject initialize(IRubyObject[] args) {
+		if (checkArgumentCount(args, 0, 2) > 0) {
+			scanner = new StringScanner(((RubyString)args[0]).getValue());
+		} else {
+			scanner = new StringScanner();
+		}
+		return this;
+	}
+	
+	public IRubyObject concat(IRubyObject obj) {
+		scanner.append(obj.convertToString().getValue());
+		return this;
+	}
+	
+	private RubyBoolean trueOrFalse(boolean p) {
+		if (p) {
+			return getRuntime().getTrue();
+		} else {
+			return getRuntime().getFalse();
+		}
+	}
+	
+	private IRubyObject positiveFixnumOrNil(int val) {
+		if (val > -1) {
+			return RubyFixnum.newFixnum(getRuntime(), (long)val);
+		} else {
+			return getRuntime().getNil();
+		}
+	}
+	
+	private IRubyObject stringOrNil(CharSequence cs) {
+		if (cs == null) {
+			return getRuntime().getNil();
+		} else {
+			return RubyString.newString(getRuntime(), cs);
+		}
+	}
+
+	public IRubyObject group(RubyFixnum num) {
+		return stringOrNil(scanner.group(RubyFixnum.fix2int(num)));
+	}
+	
+	public RubyBoolean bol_p() {
+		return trueOrFalse(scanner.isBeginningOfLine());
+	}
+	
+	public IRubyObject check(RubyRegexp rx) {
+		return stringOrNil(scanner.check(rx.getPattern()));
+	}
+	
+	public IRubyObject check_until(RubyRegexp rx) {
+		return stringOrNil(scanner.checkUntil(rx.getPattern()));
+	}
+	
+	public IRubyObject terminate() {
+		scanner.terminate();
+		return this;
+	}
+	
+	public RubyBoolean eos_p() {
+		return trueOrFalse(scanner.isEndOfString());
+	}
+	
+	public IRubyObject exist_p(RubyRegexp rx) {
+		return positiveFixnumOrNil(scanner.exists(rx.getPattern()));
+	}
+	
+	public IRubyObject getch() {
+		char c = scanner.getChar();
+		if (c == 0) {
+			return getRuntime().getNil();
+		} else {
+			return RubyString.newString(getRuntime(), new Character(c).toString());
+		}
+	}
+	
+	public IRubyObject inspect() {
+		return super.inspect();
+	}
+	
+	public IRubyObject match_p(RubyRegexp rx) {
+		return positiveFixnumOrNil(scanner.matches(rx.getPattern()));
+	}
+	
+	public IRubyObject matched() {
+		return stringOrNil(scanner.matchedValue());
+	}
+	
+	public RubyBoolean matched_p() {
+		return trueOrFalse(scanner.matched());
+	}
+	
+	public IRubyObject matched_size() {
+		return positiveFixnumOrNil(scanner.matchedSize());
+	}
+	
+	public IRubyObject peek(RubyFixnum length) {
+		return RubyString.newString(getRuntime(), scanner.peek(RubyFixnum.fix2int(length)));
+	}
+	
+	public RubyFixnum pos() {
+		return RubyFixnum.newFixnum(getRuntime(), (long)scanner.getPos());
+	}
+	 
+	public RubyFixnum set_pos(RubyFixnum pos) {
+		try {
+			scanner.setPos(RubyFixnum.fix2int(pos));
+		} catch (IllegalArgumentException e) {
+			throw getRuntime().newRangeError("index out of range");
+		}
+		return pos;
+	}
+	
+	public IRubyObject post_match() {
+		return stringOrNil(scanner.postMatch());
+	}
+	
+	public IRubyObject pre_match() {
+		return stringOrNil(scanner.preMatch());
+	}
+	
+	public IRubyObject reset() {
+		scanner.reset();
+		return this;
+	}
+	
+	public RubyString rest() {
+		return RubyString.newString(getRuntime(), scanner.rest());
+	}
+	
+	public RubyBoolean rest_p() {
+		return trueOrFalse(!scanner.isEndOfString());
+	}
+	
+	public RubyFixnum rest_size() {
+		return RubyFixnum.newFixnum(getRuntime(), (long)scanner.rest().length());
+	}
+	
+	public IRubyObject scan(RubyRegexp rx) {
+		return stringOrNil(scanner.scan(rx.getPattern()));
+	}
+	
+	public IRubyObject scan_full(RubyRegexp rx, RubyBoolean adv_ptr, RubyBoolean ret_str) {
+		if (adv_ptr.isTrue()) {
+			if (ret_str.isTrue()) {
+				return stringOrNil(scanner.scan(rx.getPattern()));
+			} else {
+				return positiveFixnumOrNil(scanner.skip(rx.getPattern()));
+			}
+		} else {
+			if (ret_str.isTrue()) {
+				return stringOrNil(scanner.check(rx.getPattern()));
+			} else {
+				return positiveFixnumOrNil(scanner.matches(rx.getPattern()));
+			}
+		}
+	}
+	
+	public IRubyObject scan_until(RubyRegexp rx) {
+		return stringOrNil(scanner.scanUntil(rx.getPattern()));
+	}
+	
+	public IRubyObject search_full(RubyRegexp rx, RubyBoolean adv_ptr, RubyBoolean ret_str) {
+		if (adv_ptr.isTrue()) {
+			if (ret_str.isTrue()) {
+				return stringOrNil(scanner.scanUntil(rx.getPattern()));
+			} else {
+				return positiveFixnumOrNil(scanner.skipUntil(rx.getPattern()));
+			}
+		} else {
+			if (ret_str.isTrue()) {
+				return stringOrNil(scanner.checkUntil(rx.getPattern()));
+			} else {
+				return positiveFixnumOrNil(scanner.exists(rx.getPattern()));
+			}
+		}
+	}
+	
+	public IRubyObject skip(RubyRegexp rx) {
+		return positiveFixnumOrNil(scanner.skip(rx.getPattern()));
+	}
+	
+	public IRubyObject skip_until(RubyRegexp rx) {
+		return positiveFixnumOrNil(scanner.skipUntil(rx.getPattern()));
+	}
+	
+	public RubyString string() {
+		return RubyString.newString(getRuntime(), scanner.getString());
+	}
+	
+	public RubyString set_string(RubyString str) {
+		scanner.setString(str.getValue());
+		return str;
+	}
+	
+	public IRubyObject unscan() {
+		scanner.unscan();
+		return this;
+	}
+}
\ No newline at end of file
Index: test/org/jruby/util/StringScannerTest.java
===================================================================
--- test/org/jruby/util/StringScannerTest.java	
+++ test/org/jruby/util/StringScannerTest.java	
@@ -0,0 +1,397 @@
+package org.jruby.util;
+
+
+import java.util.regex.Pattern;
+
+import junit.framework.TestCase;
+
+/**
+ * @author kscott
+ *
+ */
+public class StringScannerTest extends TestCase {
+	
+	private final static Pattern WORD_CHARS = Pattern.compile("\\w+");
+	private final static Pattern WHITESPACE = Pattern.compile("\\s+");
+	
+	private final static String DATE_STRING = "Fri Dec 12 1975 14:39";
+
+	/*
+	 * @see TestCase#setUp()
+	 */
+	protected void setUp() throws Exception {
+		super.setUp();
+	}
+
+	/*
+	 * @see TestCase#tearDown()
+	 */
+	protected void tearDown() throws Exception {
+		super.tearDown();
+	}
+	
+	public void testCreate() throws Exception {
+		StringScanner ss = new StringScanner("Test String");
+		
+		assertEquals("Test String", ss.getString());
+		
+		assertEquals(0, ss.getPos());
+		
+		assertFalse("matched() must return false after create", ss.matched());
+		
+		assertEquals(-1, ss.matchedSize());
+		
+		try {
+			ss.unscan();
+			fail("unscan() called after create must throw exception.");
+		} catch (IllegalStateException e) {
+			
+		}
+	}
+	
+	public void testSetString() throws Exception {
+		StringScanner ss = new StringScanner("Test String");
+		
+		ss.scan(WORD_CHARS);
+		
+		ss.setString("test string");
+		
+		assertEquals("test string", ss.getString());
+		
+		assertEquals(0, ss.getPos());
+		
+		assertFalse("matched() must return false after setString is called", ss.matched());
+		
+		assertEquals(-1, ss.matchedSize());
+		
+		try {
+			ss.unscan();
+			fail("unscan() must throw exception after setString() is called.");
+		} catch (IllegalStateException e) {
+			
+		}
+	}
+	
+	public void testEOS() throws Exception {
+		StringScanner ss = new StringScanner("Test String");
+		
+		assertFalse("New StringScanner with non-empty string returned true for isEndOfString", ss.isEndOfString());
+		
+		ss = new StringScanner("");
+		
+		assertTrue("New StringScanner with empty string returned false for isEndOfString", ss.isEndOfString());
+	}
+	
+	
+	public void testGetChar() throws Exception {
+		StringScanner ss = new StringScanner("12");
+		
+		char val = ss.getChar();
+		
+		assertEquals('1', val);
+		
+		assertFalse(ss.isEndOfString());
+		
+		val = ss.getChar();
+		
+		assertEquals('2', val);
+		assertEquals("2", ss.matchedValue());
+		
+		assertTrue(ss.isEndOfString());
+		
+		val = ss.getChar();
+		
+		assertEquals(0, val);
+	}
+	
+	public void testScan() throws Exception {
+		StringScanner ss = new StringScanner("Test String");
+
+		CharSequence cs = ss.scan(WORD_CHARS);
+		assertEquals("Test", cs);
+		assertTrue("matched() should have returned true", ss.matched());
+		assertEquals("Test", ss.matchedValue());
+		
+		cs = ss.scan(WORD_CHARS);
+		assertNull("Non match should return null", cs);
+		
+		cs = ss.scan(WHITESPACE);
+		cs = ss.scan(WORD_CHARS);
+		assertEquals("String", cs);
+		
+		assertTrue("isEndOfString should be true", ss.isEndOfString());
+	}
+	
+	public void testScanUntil() throws Exception {
+		StringScanner ss = new StringScanner(DATE_STRING);
+		
+		CharSequence cs = ss.scanUntil(Pattern.compile("1"));
+		
+		assertEquals("Fri Dec 1", cs);
+		
+		assertTrue("matched() must return true after successful scanUntil()", ss.matched());
+		
+		assertEquals(9, ss.getPos());
+		
+		assertEquals("1", ss.matchedValue());
+	}
+	
+	public void testCheckUntil() throws Exception {
+		StringScanner ss = new StringScanner(DATE_STRING);
+		
+		CharSequence cs = ss.checkUntil(Pattern.compile("1"));
+		
+		assertEquals("Fri Dec 1", cs);
+		
+		assertEquals(0, ss.getPos());
+		
+		assertEquals("1", ss.matchedValue());
+	}
+	
+	public void testSkipUntil() throws Exception {
+		StringScanner ss = new StringScanner(DATE_STRING);
+		
+		assertEquals(9, ss.skipUntil(Pattern.compile("1")));
+		
+		assertEquals("1", ss.matchedValue());
+	}
+	
+	public void testPos() throws Exception {
+		StringScanner ss = new StringScanner("word 123");
+		
+		assertEquals(0, ss.getPos());
+		
+		ss.getChar();
+		
+		assertEquals(1, ss.getPos());
+		
+		ss.scan(WORD_CHARS);
+		
+		assertEquals(4, ss.getPos());
+	}
+	
+	public void testIsBeginningOfLine() throws Exception {
+		StringScanner ss = new StringScanner("Test\nString\n");
+		
+		assertTrue(ss.isBeginningOfLine());
+		
+		ss.scan(WORD_CHARS);
+		
+		assertFalse(ss.isBeginningOfLine());
+		
+		ss.scan(WHITESPACE);
+		
+		assertTrue(ss.isBeginningOfLine());
+	}
+	
+	public void testMatched() throws Exception {
+		StringScanner ss = new StringScanner(DATE_STRING);
+		
+		assertEquals(3, ss.matches(WORD_CHARS));
+		
+		assertEquals(0, ss.getPos());
+		
+		assertEquals("Fri", ss.matchedValue());
+		
+		assertEquals(3, ss.matchedSize());
+		
+		assertTrue("matched() should have returned true", ss.matched());
+	}
+	
+	public void testCheck() throws Exception {
+		StringScanner ss = new StringScanner(DATE_STRING);
+		
+		assertEquals("Fri", ss.check(WORD_CHARS));
+		
+		assertEquals(0, ss.getPos());
+		
+		assertEquals("Fri", ss.matchedValue());
+		
+		assertEquals(3, ss.matchedSize());
+		
+		assertTrue("matched() should have returned true", ss.matched());
+	}
+	
+	public void testSkip() throws Exception {
+		StringScanner ss = new StringScanner(DATE_STRING);
+		
+		assertEquals(3, ss.skip(WORD_CHARS));
+		
+		assertEquals(-1, ss.skip(WORD_CHARS));
+	}
+	
+	public void testExists() throws Exception {
+		StringScanner ss = new StringScanner("test string");
+		
+		assertEquals(3, ss.exists(Pattern.compile("s")));
+		
+		assertEquals(0, ss.getPos());
+		
+		assertTrue(ss.matched());
+		
+		assertEquals(-1, ss.exists(Pattern.compile("z")));
+	}
+	
+	public void testUnscan() throws Exception {
+		StringScanner ss = new StringScanner(DATE_STRING);
+		
+		ss.scan(WORD_CHARS);
+		
+		ss.unscan();
+		
+		assertEquals(0, ss.getPos());
+		
+		assertFalse("matched() must return false after unscan()", ss.matched());
+		
+		assertEquals(-1, ss.matchedSize());
+		
+		assertNull("matchedValue() must return null after unscan()", ss.matchedValue());
+		
+		ss.scan(WHITESPACE);
+		
+		try {
+			ss.unscan();
+			fail("unscan() called after an unmatched scan must throw exception.");
+		} catch (IllegalStateException e) {
+			
+		}
+		
+		ss.skip(WORD_CHARS);
+		
+		ss.unscan();
+		
+		assertEquals(0, ss.getPos());
+	}
+
+	public void testAppend() throws Exception {
+		StringScanner ss = new StringScanner(DATE_STRING);
+		
+		ss.scan(WORD_CHARS);
+		
+		ss.append(" +1000 GMT");
+		
+		assertEquals(3, ss.getPos());
+		
+		assertEquals(DATE_STRING + " +1000 GMT", ss.getString());
+	}
+	
+	public void testReset() throws Exception {
+		StringScanner ss = new StringScanner(DATE_STRING);
+		
+		ss.scan(WORD_CHARS);
+		
+		ss.reset();
+		
+		assertEquals(0, ss.getPos());
+		
+		assertFalse("matched() must return false after reset()", ss.matched());
+		
+		assertEquals(-1, ss.matchedSize());
+		
+		assertNull("matchedValue() must return null after reset()", ss.matchedValue());
+		
+		try {
+			ss.unscan();
+			fail("unscan() must throw exception after reset()");
+		} catch (IllegalStateException e) {
+			
+		}
+	}
+		
+	public void testGrouping() throws Exception {
+		StringScanner ss = new StringScanner(DATE_STRING);
+		
+		ss.scan(Pattern.compile("(\\w+) (\\w+) (\\d+)"));
+		
+		assertEquals("Fri", ss.group(1));
+		
+		assertEquals("Dec", ss.group(2));
+		
+		assertEquals("12", ss.group(3));
+		
+		assertNull(ss.group(4));
+		
+		ss.scan(WORD_CHARS);
+		
+		assertNull(ss.group(1));
+		
+		char c = ss.getChar();
+		assertEquals(c, ss.group(0).charAt(0));
+	}
+	
+	public void testPrePostMatch() throws Exception {
+		StringScanner ss = new StringScanner(DATE_STRING);
+		
+		CharSequence cs = ss.scan(WORD_CHARS);
+		
+		ss.scan(WORD_CHARS);
+		assertNull(ss.preMatch());
+		assertNull(ss.postMatch());
+		
+		ss.scan(WHITESPACE);
+		
+		assertEquals("Fri", ss.preMatch());
+		assertEquals("Dec 12 1975 14:39", ss.postMatch());
+		
+		ss.getChar();
+		
+		assertEquals("Fri ", ss.preMatch());
+		assertEquals("ec 12 1975 14:39", ss.postMatch());
+		
+		ss.reset();
+		
+		ss.scanUntil(Pattern.compile("1"));
+		
+		assertEquals("Fri Dec ", ss.preMatch());
+		assertEquals("2 1975 14:39", ss.postMatch());
+	}
+	
+	public void testTerminate() throws Exception {
+		StringScanner ss = new StringScanner(DATE_STRING);
+		
+		ss.scan(WORD_CHARS);
+		
+		ss.terminate();
+		
+		assertTrue("endOfString() should return true after terminate().", ss.isEndOfString());
+		
+		assertFalse("matched() should return false after terminate().", ss.matched());
+	}
+	
+	public void testPeek() throws Exception {
+		StringScanner ss = new StringScanner("test string");
+		
+		assertEquals("test st", ss.peek(7));
+		
+		assertEquals(0, ss.getPos());
+		
+		assertEquals("test string", ss.peek(300));
+	}
+	
+	public void testRest() throws Exception {
+		StringScanner ss = new StringScanner("test string");
+		
+		ss.scan(WORD_CHARS);
+		
+		assertEquals(" string", ss.rest());
+		
+		ss.terminate();
+		
+		assertEquals("", ss.rest());
+	}
+	
+	public void testSetPos() throws Exception {
+		StringScanner ss = new StringScanner("test string");
+		
+		ss.setPos(5);
+		
+		assertEquals("string", ss.rest());
+		
+		try {
+			ss.setPos(300);
+			fail("setPos() with value greater than the length of the string should throw exception.");
+		} catch (IllegalArgumentException e) {
+			
+		}
+	}
+}
Index: test/testStringScan.rb
===================================================================
--- test/testStringScan.rb	(revision 2110)
+++ test/testStringScan.rb	(working copy)
@@ -8,6 +8,7 @@
 test_equal("Fri", s[1])
 test_equal("Dec", s[2])
 test_equal("12", s[3])
+test_equal(nil, s[4])
 test_equal("1975 14:39", s.post_match)
 test_equal("", s.pre_match)
 
@@ -36,6 +37,7 @@
 s = StringScanner.new('test string')
 test_equal(7, s.pos = 7)
 test_equal("ring", s.rest)
+test_exception(RangeError) { s.pos = 20 }
 
 ##### scan ######
 
@@ -66,3 +68,24 @@
 test_equal(6, s.skip(/\w+/))
 test_equal(nil, s.skip(/./))
 
+##### scan_full, search_full ######
+s = StringScanner.new('test string')
+test_equal(4, s.scan_full(/\w+/, true, false))
+test_equal(4, s.pos)
+test_equal("test", s.matched)
+s.reset
+test_equal("test", s.scan_full(/\w+/, true, true))
+test_equal(4, s.pos)
+s.reset
+test_equal("test", s.scan_full(/\w+/, false, true))
+test_equal(0, s.pos)
+s.reset
+test_equal("test str", s.search_full(/r/, true, true))
+test_equal(8, s.pos)
+s.reset
+test_equal(8, s.search_full(/r/, true, false))
+test_equal(8, s.pos)
+test_equal("r", s.matched)
+s.reset
+test_equal("test str", s.search_full(/r/, false, true))
+test_equal(0, s.pos)
\ No newline at end of file
