From 65df43c693cc54b44f9bd167489aea842b23e964 Mon Sep 17 00:00:00 2001 From: Martin Fenner Date: Sat, 10 Dec 2016 21:44:03 +0000 Subject: [PATCH] added optional checksum --- README.markdown | 8 ++++--- lib/base32/crockford.rb | 45 ++++++++++++++++++++++++++--------- test/test_base32_crockford.rb | 34 +++++++++++++++++++++++++- 3 files changed, 72 insertions(+), 15 deletions(-) diff --git a/README.markdown b/README.markdown index c01971f..5f9ce1b 100644 --- a/README.markdown +++ b/README.markdown @@ -9,7 +9,7 @@ Installation Changes ======= - + 0.2.0 - added optional checksum 0.1.0 - rename gem to base32-crockford 0.0.2 - ruby 1.9 compatibility 0.0.1 - initial release @@ -18,9 +18,11 @@ Usage ===== #!/usr/bin/env ruby - + require 'base32/crockford' - + Base32::Crockford.encode(1234) # => "16J" Base32::Crockford.encode(100**10, :split=>5, :length=>15) # => "02PQH-TY5NH-H0000" Base32::Crockford.decode("2pqh-ty5nh-hoooo") # => 10**100 + Base32::Crockford.encode(1234, checksum: true) # => "16JD" + Base32::Crockford.decode("16JD", checksum: true) # => 1234 diff --git a/lib/base32/crockford.rb b/lib/base32/crockford.rb index 7d6132e..bbc9db6 100644 --- a/lib/base32/crockford.rb +++ b/lib/base32/crockford.rb @@ -41,17 +41,23 @@ module Base32 # # class Base32::Crockford - VERSION = "0.1.0" + VERSION = "0.2.0" ENCODE_CHARS = %w(0 1 2 3 4 5 6 7 8 9 A B C D E F G H J K M N P Q R S T V W X Y Z ?) + CHECKSUM_MAP = { "*" => 32, "~" => 33, "$" => 34, "=" => 35, "U" => 36 } + DECODE_MAP = ENCODE_CHARS.to_enum(:each_with_index).inject({}) do |h,(c,i)| h[c] = i; h end.merge({'I' => 1, 'L' => 1, 'O' => 0}) # encodes an integer into a string # + # when +checksum+ is given, a checksum is added at the end of the the string, + # calculated as modulo 37 of +number+. Five additional checksum symbols are + # used for symbol values 32-36 + # # when +split+ is given a hyphen is inserted every characters to improve # readability # @@ -63,12 +69,14 @@ class Base32::Crockford # def self.encode(number, opts = {}) # verify options - raise ArgumentError unless (opts.keys - [:length, :split] == []) + raise ArgumentError unless (opts.keys - [:length, :split, :checksum] == []) str = number.to_s(2).reverse.scan(/.{1,5}/).map do |bits| ENCODE_CHARS[bits.reverse.to_i(2)] end.reverse.join + str = str + ENCODE_CHARS[number % 37] if opts[:checksum] + str = str.rjust(opts[:length], '0') if opts[:length] if opts[:split] @@ -93,17 +101,26 @@ def self.encode(number, opts = {}) # Base32::Crockford.decode("3G923-0VQVS") # => 123456789012345 # # returns +nil+ if the string contains invalid characters and can't be - # decoded + # decoded, or if checksum option is used and checksum is incorrect # - def self.decode(string) - clean(string).split(//).map { |char| + def self.decode(string, opts = {}) + if opts[:checksum] + checksum_char = string.slice!(-1) + checksum_number = DECODE_MAP.merge(CHECKSUM_MAP)[checksum_char] + end + + number = clean(string).split(//).map { |char| DECODE_MAP[char] or return nil }.inject(0) { |result,val| (result << 5) + val } + + number % 37 == checksum_number or return nil if opts[:checksum] + + number end # same as decode, but raises ArgumentError when the string can't be decoded # - def self.decode!(string) + def self.decode!(string, opts = {}) decode(string) or raise ArgumentError end @@ -112,17 +129,23 @@ def self.decode!(string) # # replaces invalid characters with a question mark ('?') # - def self.normalize(string) - clean(string).split(//).map { |char| + def self.normalize(string, opts = {}) + checksum_char = string.slice!(-1) if opts[:checksum] + + string = clean(string).split(//).map { |char| ENCODE_CHARS[DECODE_MAP[char] || 32] }.join + + string = string + checksum_char if opts[:checksum] + + string end - # returns false iff the string contains invalid characters and can't be + # returns false if the string contains invalid characters and can't be # decoded # - def self.valid?(string) - !(normalize(string) =~ /\?/) + def self.valid?(string, opts = {}) + !(normalize(string, opts) =~ /\?/) end class << self diff --git a/test/test_base32_crockford.rb b/test/test_base32_crockford.rb index 5b25dd8..71507be 100644 --- a/test/test_base32_crockford.rb +++ b/test/test_base32_crockford.rb @@ -48,11 +48,21 @@ def test_normalize assert_equal "B?123", Base32::Crockford.normalize("BU-123") end + def test_normalize_with_checksum + assert_equal "B?123", Base32::Crockford.normalize("BU-123", :checksum => true) + assert_equal "B123U", Base32::Crockford.normalize("B123U", :checksum => true) + end + def test_valid assert_equal true, Base32::Crockford.valid?("hello-world") assert_equal false, Base32::Crockford.valid?("BU-123") end + def test_valid_with_checksum + assert_equal true, Base32::Crockford.valid?("B123U", :checksum => true) + assert_equal false, Base32::Crockford.valid?("BU-123", :checksum => true) + end + def test_length_and_hyphenization assert_equal "0016J", Base32::Crockford.encode(1234, :length => 5) assert_equal "0-01-6J", @@ -60,5 +70,27 @@ def test_length_and_hyphenization assert_equal "00-010", Base32::Crockford.encode(32, :length => 5, :split => 3) end -end + def test_encoding_checksum + assert_equal "16JD", + Base32::Crockford.encode(1234, :checksum => true) + assert_equal "016JD", + Base32::Crockford.encode(1234, :length => 5, :checksum => true) + assert_equal "0-16-JD", + Base32::Crockford.encode(1234, :length => 5, :split => 2, :checksum => true) + end + + def test_decoding_checksum + assert_equal 1234, + Base32::Crockford.decode("16JD", :checksum => true) + assert_equal 1234, + Base32::Crockford.decode("016JD", :length => 5, :checksum => true) + assert_equal 1234, + Base32::Crockford.decode("0-16-JD", :length => 5, :split => 2, :checksum => true) + end + + def test_decoding_invalid_checksum + assert_equal nil, + Base32::Crockford.decode("16JC", :checksum => true) + end +end