diff --git a/app/views/shared/_entry.erb b/app/views/shared/_entry.erb index 79e494f..07a6320 100644 --- a/app/views/shared/_entry.erb +++ b/app/views/shared/_entry.erb @@ -8,7 +8,7 @@
-<% VimGolf::Keylog.new(entry.script).each do |key| %><% if key.size > 1 %><%= key %><% else %><%= key %><% end %><% end %>
+<% VimGolf::Keylog.new(entry.script, entry.created_at).each do |key| %><% if key.size > 1 %><%= key %><% else %><%= key %><% end %><% end %>
 
<% entry.comments.each do |c| %> diff --git a/lib/vimgolf/lib/vimgolf/challenge.rb b/lib/vimgolf/lib/vimgolf/challenge.rb index 25996f4..1f3c368 100644 --- a/lib/vimgolf/lib/vimgolf/challenge.rb +++ b/lib/vimgolf/lib/vimgolf/challenge.rb @@ -91,7 +91,7 @@ def upload proxy.start(url.host, url.port) do |http| request = Net::HTTP::Post.new(url.request_uri) - request.set_form_data({"challenge_id" => @id, "apikey" => Config.load['key'], "entry" => IO.read(log_path)}) + request.set_form_data({"challenge_id" => @id, "apikey" => Config.load['key'], "entry" => IO.binread(log_path)}) request["Accept"] = "application/json" res = http.request(request) diff --git a/lib/vimgolf/lib/vimgolf/cli.rb b/lib/vimgolf/lib/vimgolf/cli.rb index 44fcff8..96fb859 100644 --- a/lib/vimgolf/lib/vimgolf/cli.rb +++ b/lib/vimgolf/lib/vimgolf/cli.rb @@ -114,7 +114,7 @@ def play(challenge) system(*vimcmd) # assembled as an array, bypasses the shell if $?.exitstatus.zero? - log = Keylog.new(IO.read(challenge.log_path)) + log = Keylog.new(IO.binread(challenge.log_path)) VimGolf.ui.info "\nHere are your keystrokes:" VimGolf.ui.print_log log diff --git a/lib/vimgolf/lib/vimgolf/keylog.rb b/lib/vimgolf/lib/vimgolf/keylog.rb index ee79b40..3a94767 100644 --- a/lib/vimgolf/lib/vimgolf/keylog.rb +++ b/lib/vimgolf/lib/vimgolf/keylog.rb @@ -1,258 +1,268 @@ -# encoding: UTF-8 +# encoding: ASCII-8BIT +# Force encoding of string literals. Must match solution text. module VimGolf class Keylog include Enumerable - alias_method :convert , :to_s - alias_method :score , :count - - def initialize(input) - @input = input + def initialize(input, time=Time.now.utc) + # Force encoding of solution text. Must match string literals. + # .force_encoding CHANGES THE ORIGINAL STRING! + @input = input.force_encoding(Encoding::ASCII_8BIT) + @time = time end def to_s(sep = '') to_a.join(sep) end + alias_method :convert , :to_s + alias_method :score , :count + def each scanner = StringScanner.new(@input) - output = "" - - until scanner.eos? - c = scanner.get_byte - n = c.unpack('C').first - - out_char = \ - case n - - # Special platform-independent encoding stuff - when 0x80 - code = scanner.get_byte + scanner.get_byte - # This list has been populated by looking at - # :h terminal-options and vim source files: - # keymap.h and misc2.c - case code - when "k1"; "" - when "k2"; "" - when "k3"; "" - when "k4"; "" - when "k5"; "" - when "k6"; "" - when "k7"; "" - when "k8"; "" - when "k9"; "" - when "k;"; "" - when "F1"; "" - when "F2"; "" - when "F3"; "" - when "F4"; "" - when "F5"; "" - when "F6"; "" - when "F7"; "" - when "F8"; "" - when "F9"; "" - - when "%1"; "" - when "&8"; "" - when "#2"; "" - when "*7"; "" - when "K1"; "" - when "K4"; "" - when "K3"; "" - when "K5"; "" - when "K6"; "" - when "K7"; "" - when "K8"; "" - when "K9"; "" - when "KA"; "" - when "KB"; "" - when "KC"; "" - when "KD"; "" - when "KE"; "" - when "KF"; "" - when "KG"; "" - when "KH"; "" - when "KI"; "" - when "KJ"; "" - when "KK"; "" - when "KL"; "" - - when "kP"; "" - when "kN"; "" - when "kh"; "" - when "@7"; "" - when "kI"; "" - when "kD"; "" - when "kb"; "" + # A Vim keycode is either a single byte, or a 3-byte sequence starting + # with 0x80. + while (c = scanner.get_byte) + n = c.ord + if n == 0x80 + b2, b3 = scanner.get_byte, scanner.get_byte + if b2 == "\xfd" && b3 >= "\x38" && @time > SNIFF_DATE + # Should we account for KE_SNIFF removal? + b3 = (b3.ord + 1).chr + end + code = KC_MBYTE[b2+b3] + yield code if code # ignore "nil" keystrokes (like window focus) + else + yield KC_1BYTE[n] + end + end + end - when "ku"; "" - when "kd"; "" - when "kl"; "" - when "kr"; "" - when "#4"; "" - when "%i"; "" + # Quick lookup array for single-byte keycodes + KC_1BYTE = [] + (0..255).each {|n| KC_1BYTE.push("<%#04x>" % n)} # Fallback for non-ASCII + (1..127).each {|n| KC_1BYTE[n] = ""} + (32..126).each {|c| KC_1BYTE[c] = c.chr } # Printing chars + KC_1BYTE[0x1b] = "" # Special names for a few control chars + KC_1BYTE[0x0d] = "" + KC_1BYTE[0x0a] = "" + KC_1BYTE[0x09] = "" - when "kB"; "" - when "\xffX"; "" + # After this date, assume KE_SNIFF is removed + SNIFF_DATE = Time.utc(2016, 4) - # This is how you escape literal 0x80 - when "\xfeX"; "<0x80>" + KC_MBYTE = Hash.new do |_h,k| + '<' + k.bytes.map {|b| "%02x" % b}.join('-') + '>' # For missing keycodes + end.update({ + # This list has been populated by looking at + # :h terminal-options and vim source files: + # keymap.h and misc2.c + "k1" => "", + "k2" => "", + "k3" => "", + "k4" => "", + "k5" => "", + "k6" => "", + "k7" => "", + "k8" => "", + "k9" => "", + "k;" => "", + "F1" => "", + "F2" => "", + "F3" => "", + "F4" => "", + "F5" => "", + "F6" => "", + "F7" => "", + "F8" => "", + "F9" => "", - # These rarely-used modifiers should be combined with the next - # stroke (like ), but let's put them here for now - when "\xfc\x02"; "" - when "\xfc\x04"; "" - when "\xfc\x06"; "" - when "\xfc\x08"; "" - when "\xfc\x0a"; "" - when "\xfc\x0c"; "" - when "\xfc\x0e"; "" - when "\xfc\x10"; "" - when "\xfc\x12"; "" - when "\xfc\x14"; "" - when "\xfc\x16"; "" - when "\xfc\x18"; "" - when "\xfc\x1a"; "" - when "\xfc\x1c"; "" - when "\xfc\x1e"; "" + "%1" => "", + "&8" => "", + "#2" => "", + "*7" => "", + "K1" => "", + "K4" => "", + "K3" => "", + "K5" => "", + "K6" => "", + "K7" => "", + "K8" => "", + "K9" => "", + "KA" => "", + "KB" => "", + "KC" => "", + "KD" => "", + "KE" => "", + "KF" => "", + "KG" => "", + "KH" => "", + "KI" => "", + "KJ" => "", + "KK" => "", + "KL" => "", - when "\xfd\x4"; "" - when "\xfd\x5"; "" - when "\xfd\x6"; "" - when "\xfd\x7"; "" - when "\xfd\x8"; "" - when "\xfd\x9"; "" - when "\xfd\xa"; "" - when "\xfd\xb"; "" - when "\xfd\xc"; "" - when "\xfd\xd"; "" - when "\xfd\xe"; "" - when "\xfd\xf"; "" - when "\xfd\x10"; "" - when "\xfd\x11"; "" - when "\xfd\x12"; "" - when "\xfd\x13"; "" - when "\xfd\x14"; "" - when "\xfd\x15"; "" - when "\xfd\x16"; "" - when "\xfd\x17"; "" - when "\xfd\x18"; "" - when "\xfd\x19"; "" - when "\xfd\x1a"; "" - when "\xfd\x1b"; "" - when "\xfd\x1c"; "" - when "\xfd\x1d"; "" - when "\xfd\x1e"; "" - when "\xfd\x1f"; "" - when "\xfd\x20"; "" - when "\xfd\x21"; "" - when "\xfd\x22"; "" - when "\xfd\x23"; "" - when "\xfd\x24"; "" - when "\xfd\x25"; "" - when "\xfd\x26"; "" - when "\xfd\x27"; "" - when "\xfd\x28"; "" - when "\xfd\x29"; "" - when "\xfd\x2a"; "" - when "\xfd\x2b"; "" - when "\xfd\x2c"; "" - when "\xfd\x2d"; "" - when "\xfd\x2e"; "" - when "\xfd\x2f"; "" - when "\xfd\x30"; "" - when "\xfd\x31"; "" - when "\xfd\x32"; "" - when "\xfd\x33"; "" - when "\xfd\x34"; "" - when "\xfd\x35"; nil # KE_IGNORE - #when "\xfd\x36"; "KE_TAB" - #when "\xfd\x37"; "KE_S_TAB_OLD" - #when "\xfd\x38"; "KE_SNIFF" - #when "\xfd\x39"; "KE_XF1" - #when "\xfd\x3a"; "KE_XF2" - #when "\xfd\x3b"; "KE_XF3" - #when "\xfd\x3c"; "KE_XF4" - #when "\xfd\x3d"; "KE_XEND" - #when "\xfd\x3e"; "KE_ZEND" - #when "\xfd\x3f"; "KE_XHOME" - #when "\xfd\x40"; "KE_ZHOME" - #when "\xfd\x41"; "KE_XUP" - #when "\xfd\x42"; "KE_XDOWN" - #when "\xfd\x43"; "KE_XLEFT" - #when "\xfd\x44"; "KE_XRIGHT" - #when "\xfd\x45"; "KE_LEFTMOUSE_NM" - #when "\xfd\x46"; "KE_LEFTRELEASE_NM" - #when "\xfd\x47"; "KE_S_XF1" - #when "\xfd\x48"; "KE_S_XF2" - #when "\xfd\x49"; "KE_S_XF3" - #when "\xfd\x4a"; "KE_S_XF4" - when "\xfd\x4b"; "" - when "\xfd\x4c"; "" + "kP" => "", + "kN" => "", + "kh" => "", + "@7" => "", + "kI" => "", + "kD" => "", + "kb" => "", - # Horizontal scroll wheel support was added in Vim 7.3c. These - # 2 entries shifted the rest of the KS_EXTRA mappings down 2. - # Though Vim 7.2 is rare today, it was common soon after - # vimgolf.com was launched. In cases where the 7.3 code is - # never used but the 7.2 code was common, it makes sense to use - # the 7.2 code. There are conflicts though, so some legacy - # keycodes have to stay wrong. - when "\xfd\x4d"; "" - when "\xfd\x4e"; "" - when "\xfd\x4f"; "" - when "\xfd\x50"; "" - when "\xfd\x51"; "<0x9b>" # :help - #when "\xfd\x52"; "KE_SNR" - #when "\xfd\x53"; "KE_PLUG" # never used - when "\xfd\x53"; "" # 7.2 compat - #when "\xfd\x54"; "KE_CMDWIN" # never used - when "\xfd\x54"; "" # 7.2 compat - when "\xfd\x55"; "" # 7.2 conflict - when "\xfd\x56"; "" # 7.2 conflict - when "\xfd\x57"; "" - when "\xfd\x58"; "" - #when "\xfd\x59"; "KE_X1MOUSE" - #when "\xfd\x5a"; "KE_X1DRAG" - #when "\xfd\x5b"; "KE_X1RELEASE" - #when "\xfd\x5c"; "KE_X2MOUSE" - #when "\xfd\x5d"; "KE_X2DRAG" - #when "\xfd\x5e"; "KE_X2RELEASE" - when "\xfd\x5e"; nil # 7.2 compat (I think?) - #when "\xfd\x5f"; "KE_DROP" - #when "\xfd\x60"; "KE_CURSORHOLD" - when "\xfd\x60"; nil # 7.2 Focus Gained compat - #when "\xfd\x61"; "KE_NOP" - when "\xfd\x62"; nil # Focus Gained (GVIM) - when "\xfd\x63"; nil # Focus Lost (GVIM) + "ku" => "", + "kd" => "", + "kl" => "", + "kr" => "", + "#4" => "", + "%i" => "", - else - #puts "Unknown Vim code: #{code.inspect}" - '<%02x-%02x>' % code.unpack('CC') - end + "kB" => "", + "\xffX" => "", - # Printable ASCII - when 32..126; c + # This is how you escape literal 0x80 + "\xfeX" => "<0x80>", - # Control characters with special names - when 0; "" - when 9; "" - when 10; "" - when 13; "" - when 27; "" + # These rarely-used modifiers should be combined with the next + # stroke (like ), but let's put them here for now + "\xfc\x02" => "", + "\xfc\x04" => "", + "\xfc\x06" => "", + "\xfc\x08" => "", + "\xfc\x0a" => "", + "\xfc\x0c" => "", + "\xfc\x0e" => "", + "\xfc\x10" => "", + "\xfc\x12" => "", + "\xfc\x14" => "", + "\xfc\x16" => "", + "\xfc\x18" => "", + "\xfc\x1a" => "", + "\xfc\x1c" => "", + "\xfc\x1e" => "", - # Otherwise, use format. Flip bit 7 - when 0..127; "" + # KS_EXTRA keycodes (starting with 0x80 0xfd) are defined by an enum in + # Vim's keymap.h. Sometimes, a new Vim adds or removes a keycode, which + # changes the binary representation of every keycode after it. Very + # annoying. + "\xfd\x4" => "", + "\xfd\x5" => "", + "\xfd\x6" => "", + "\xfd\x7" => "", + "\xfd\x8" => "", + "\xfd\x9" => "", + "\xfd\xa" => "", + "\xfd\xb" => "", + "\xfd\xc" => "", + "\xfd\xd" => "", + "\xfd\xe" => "", + "\xfd\xf" => "", + "\xfd\x10" => "", + "\xfd\x11" => "", + "\xfd\x12" => "", + "\xfd\x13" => "", + "\xfd\x14" => "", + "\xfd\x15" => "", + "\xfd\x16" => "", + "\xfd\x17" => "", + "\xfd\x18" => "", + "\xfd\x19" => "", + "\xfd\x1a" => "", + "\xfd\x1b" => "", + "\xfd\x1c" => "", + "\xfd\x1d" => "", + "\xfd\x1e" => "", + "\xfd\x1f" => "", + "\xfd\x20" => "", + "\xfd\x21" => "", + "\xfd\x22" => "", + "\xfd\x23" => "", + "\xfd\x24" => "", + "\xfd\x25" => "", + "\xfd\x26" => "", + "\xfd\x27" => "", + "\xfd\x28" => "", + "\xfd\x29" => "", + "\xfd\x2a" => "", + "\xfd\x2b" => "", + "\xfd\x2c" => "", + "\xfd\x2d" => "", + "\xfd\x2e" => "", + "\xfd\x2f" => "", + "\xfd\x30" => "", + "\xfd\x31" => "", + "\xfd\x32" => "", + "\xfd\x33" => "", + "\xfd\x34" => "", + "\xfd\x35" => nil, # KE_IGNORE + #"\xfd\x36" => "KE_TAB", + #"\xfd\x37" => "KE_S_TAB_OLD", - else - #puts "Unexpected extended ASCII: #{'%#04x' % n}" - '<%#04x>' % n + # Vim 7.4.1433 removed KE_SNIFF. Unfortunately, this changed the + # offset of every keycode after it. Keycodes after this point should be + # accurate BEFORE that change. + #"\xfd\x38" => "KE_SNIFF", + #"\xfd\x39" => "KE_XF1", + #"\xfd\x3a" => "KE_XF2", + #"\xfd\x3b" => "KE_XF3", + #"\xfd\x3c" => "KE_XF4", + #"\xfd\x3d" => "KE_XEND", + #"\xfd\x3e" => "KE_ZEND", + #"\xfd\x3f" => "KE_XHOME", + #"\xfd\x40" => "KE_ZHOME", + #"\xfd\x41" => "KE_XUP", + #"\xfd\x42" => "KE_XDOWN", + #"\xfd\x43" => "KE_XLEFT", + #"\xfd\x44" => "KE_XRIGHT", + #"\xfd\x45" => "KE_LEFTMOUSE_NM", + #"\xfd\x46" => "KE_LEFTRELEASE_NM", + #"\xfd\x47" => "KE_S_XF1", + #"\xfd\x48" => "KE_S_XF2", + #"\xfd\x49" => "KE_S_XF3", + #"\xfd\x4a" => "KE_S_XF4", + "\xfd\x4b" => "", + "\xfd\x4c" => "", - end + # Horizontal scroll wheel support was added in Vim 7.3c. These + # 2 entries shifted the rest of the KS_EXTRA mappings down 2. + # Though Vim 7.2 is rare today, it was common soon after + # vimgolf.com was launched. In cases where the 7.3 code is + # never used but the 7.2 code was common, it makes sense to use + # the 7.2 code. There are conflicts though, so some legacy + # keycodes have to stay wrong. + "\xfd\x4d" => "", + "\xfd\x4e" => "", + "\xfd\x4f" => "", + "\xfd\x50" => "", + "\xfd\x51" => "<0x9b>", # :help + #"\xfd\x52" => "KE_SNR", + #"\xfd\x53" => "KE_PLUG", # never used + "\xfd\x53" => "", # 7.2 compat + #"\xfd\x54" => "KE_CMDWIN", # never used + "\xfd\x54" => "", # 7.2 compat + "\xfd\x55" => "", # 7.2 conflict + "\xfd\x56" => "", # 7.2 conflict + "\xfd\x57" => "", + "\xfd\x58" => "", + #"\xfd\x59" => "KE_X1MOUSE", + #"\xfd\x5a" => "KE_X1DRAG", + #"\xfd\x5b" => "KE_X1RELEASE", + #"\xfd\x5c" => "KE_X2MOUSE", + #"\xfd\x5d" => "KE_X2DRAG", + #"\xfd\x5e" => "KE_X2RELEASE", + "\xfd\x5e" => nil, # 7.2 compat (I think?) + #"\xfd\x5f" => "KE_DROP", + #"\xfd\x60" => "KE_CURSORHOLD", - yield out_char if out_char - end - end + # If you use gvim, you'll get an entry in your keylog every time the + # window gains or loses focus. These "keystrokes" should not show and + # should not be counted. + "\xfd\x60" => nil, # 7.2 Focus Gained compat + "\xfd\x61" => nil, # Focus Gained (GVIM) (>7.4.1433) + "\xfd\x62" => nil, # Focus Gained (GVIM) + "\xfd\x63" => nil, # Focus Lost (GVIM) + }) end end diff --git a/spec/lib/keylog_spec.rb b/spec/lib/keylog_spec.rb index b1921b6..5bc1c40 100644 --- a/spec/lib/keylog_spec.rb +++ b/spec/lib/keylog_spec.rb @@ -1,3 +1,5 @@ +# encoding: UTF-8 +# Ruby 1.9 doesn't like our fancy string literals without this. require "cli_helper" include VimGolf @@ -13,4 +15,58 @@ expect { Keylog.new(IO.read(f)).convert }.not_to raise_error end end + + it "should correctly parse and count a variety of keycodes" do + input = "\x01\x09\x1a\x1b\x1c\x1e !09\\\"'()Aa~\x7f\x80KC\x80kd\x80\xffXZZ" + output = " !09\\\"'()Aa~ZZ" + log = Keylog.new(input) + expect(log.convert).to eql(output) + expect(log.score).to eql(24) + end + + it "should be resilient to encoding mismatches" do + text = "Здравствуйте ¡Olé! おはよう \x80\xfdQ\x80\xfeX".force_encoding("UTF-8") + # .force_encoding CHANGES THE ORIGINAL STRING! + bytes = text.dup.force_encoding(Encoding::ASCII_8BIT) + + # Sanity check. Different rubies conspiring to mess up the test encodings. + expect(bytes.encoding).not_to eql(text.encoding) + # Not testing the actual output, just checking whether encoding matters + expect(Keylog.new(text).convert).to eql(Keylog.new(bytes).convert) + end + + it "should treat newline characters literally" do + # Depending on your parsing strategy, could be a problem + input = "a\ra\na\r\na\n\r\n" + output = "aaaa" + + log = Keylog.new(input) + expect(log.convert).to eql(output) + end + + it "should ignore certain non-keystroke keycodes" do + input = "\x80\xfd\x35\x80\xfdbhello\x80\xfd\x61\x80\xfd\x62 world" + output = "hello world" + + log = Keylog.new(input) + expect(log.convert).to eql(output) + expect(log.score).to eql(11) + end + + it "should handle unexpected keycodes with a fallback" do + input = "\x80 hello" + output = "<20-20>hello" + + log = Keylog.new(input) + expect(log.convert).to eql(output) + end + + it "should parse some keycodes differently depending on date submitted" do + early = "\x80\xfd\x55\x80\xfd\x56\x80\xfd\x57\x80\xfd\x58\x80\xfd\x2c" + late = "\x80\xfd\x54\x80\xfd\x55\x80\xfd\x56\x80\xfd\x57\x80\xfd\x2c" + output = "" + + expect(Keylog.new(early, Time.utc(2016, 3)).convert).to eql(output) + expect(Keylog.new(late, Time.utc(2016, 5)).convert).to eql(output) + end end