scatter_swap 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format doc
data/README.md CHANGED
@@ -1,5 +1,55 @@
1
1
  # ScatterSwap
2
2
 
3
+ This is the hashing function behind ObfuscateId.
4
+ https://github.com/namick/obfuscate_id
5
+
6
+ Designing a hash function is a bit of a black art and
7
+ being that I don't have math background, I must resort
8
+ to this simplistic swaping and scattering of array elements.
9
+
10
+ After writing this and reading/learning some elemental hashing techniques,
11
+ I realize this library is what is known as a Minimal perfect hash function:
12
+ http://en.wikipedia.org/wiki/Perfect_hash_function#Minimal_perfect_hash_function
13
+
14
+ I welcome all improvements :-)
15
+
16
+ If you have some comments or suggestions, please contact me on github
17
+ https://github.com/namick
18
+
19
+ - nathan amick
20
+
21
+
22
+ This library is built for integers that can be expressed with 10 digits:
23
+ It zero pads smaller numbers... so the number 1 is expressed with:
24
+ 0000000001
25
+
26
+ The biggest number it can deal with is:
27
+ 9999999999
28
+
29
+ Since we are working with a limited sequential set of input integers, 10 billion,
30
+ this algorithm will suffice for simple id obfuscation for many web apps.
31
+ The largest value that Ruby on Rails default id, Mysql INT type, is just over 2 billion (2147483647)
32
+ which is the same as 2 to the power of 31 minus 1, but considerably less than 10 billion.
33
+
34
+ ScatterSwap is an integer hash function designed to have:
35
+ - zero collisions ( http://en.wikipedia.org/wiki/Perfect_hash_function )
36
+ - achieve avalanche ( http://en.wikipedia.org/wiki/Avalanche_effect )
37
+ - reversable
38
+
39
+ We do that by combining two distinct strategies.
40
+
41
+ 1. Scattering - whereby we take the whole number, slice it up into 10 digits
42
+ and rearange their places, yet retaining the same 10 digits. This allows
43
+ us to preserve the sum of all the digits, regardless of order. This sum acts
44
+ as a key to reverse this scattering effect.
45
+
46
+ 2. Swapping - when dealing with many sequential numbers that we don't want
47
+ to look similar, scattering wont do us much good because so many of the
48
+ digits are the same; it deoesn't help to scatter 9 zeros around, so we need
49
+ to swap out each of those zeros for something else.. something different
50
+ for each place in the 10 digit array; for this, we need a map so that we
51
+ can reverse it.
52
+
3
53
  TODO: Write a gem description
4
54
 
5
55
  ## Installation
@@ -0,0 +1,81 @@
1
+ module ScatterSwap
2
+ class Hasher
3
+ attr_accessor :working_array
4
+
5
+ def initialize(original_integer, spin = 0)
6
+ @original_integer = original_integer
7
+ @spin = spin
8
+ zero_pad = original_integer.to_s.rjust(10, '0')
9
+ @working_array = zero_pad.split("").collect {|d| d.to_i}
10
+ end
11
+
12
+ # obfuscates an integer up to 10 digits in length
13
+ def hash
14
+ swap
15
+ scatter
16
+ completed_string
17
+ end
18
+
19
+ # de-obfuscates an integer
20
+ def reverse_hash
21
+ unscatter
22
+ unswap
23
+ completed_string
24
+ end
25
+
26
+ def completed_string
27
+ @working_array.join
28
+ end
29
+
30
+ # We want a unique map for each place in the original number
31
+ def swapper_map(index)
32
+ array = (0..9).to_a
33
+ 10.times.collect.with_index do |i|
34
+ array.rotate!(index + i ^ spin).pop
35
+ end
36
+ end
37
+
38
+ # Using a unique map for each of the ten places,
39
+ # we swap out one number for another
40
+ def swap
41
+ @working_array = @working_array.collect.with_index do |digit, index|
42
+ swapper_map(index)[digit]
43
+ end
44
+ end
45
+
46
+ # Reverse swap
47
+ def unswap
48
+ @working_array = @working_array.collect.with_index do |digit, index|
49
+ swapper_map(index).rindex(digit)
50
+ end
51
+ end
52
+
53
+ # Rearrange the order of each digit in a reversable way by using the
54
+ # sum of the digits (which doesn't change regardless of order)
55
+ # as a key to record how they were scattered
56
+ def scatter
57
+ sum_of_digits = @working_array.inject(:+).to_i
58
+ @working_array = 10.times.collect do
59
+ @working_array.rotate!(spin ^ sum_of_digits).pop
60
+ end
61
+ end
62
+
63
+ # Reverse the scatter
64
+ def unscatter
65
+ scattered_array = @working_array
66
+ sum_of_digits = scattered_array.inject(:+).to_i
67
+ @working_array = []
68
+ @working_array.tap do |unscatter|
69
+ 10.times do
70
+ unscatter.push scattered_array.pop
71
+ unscatter.rotate! (sum_of_digits ^ spin) * -1
72
+ end
73
+ end
74
+ end
75
+
76
+ # Add some spice so that different apps can have differently mapped hashes
77
+ def spin
78
+ @spin || 0
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,93 @@
1
+ require 'scatter_swap'
2
+
3
+ # This file is spike code and is not part of the library
4
+ #
5
+ # While developing this, I used this file to visualize what was going on with the numbers
6
+ #
7
+ # You can uncomment various methods at the bottom and then run it like this:
8
+ #
9
+ # watch -n1 ruby run_scatter_swap.rb
10
+ #
11
+ # tweak the code a bit and see instant visual changes in the generated numbers
12
+
13
+ def visualize_scatter_and_unscatter
14
+ # change this number to experiment with different values
15
+ rotations = 99
16
+
17
+ original = ScatterSwap.arrayify(123456789)
18
+ scattered = []
19
+ unscattered = []
20
+ puts original.join
21
+ puts "rotate!(#{rotations})"
22
+ 10.times do
23
+ puts original.rotate!(rotations).join + "->" + scattered.push(original.pop).join
24
+ end
25
+ puts "scattered"
26
+ puts scattered.join
27
+ 10.times do
28
+ puts unscattered.push(scattered.pop).join + "->" + unscattered.rotate!(rotations * -1).join
29
+ end
30
+
31
+ puts unscattered.join
32
+ end
33
+
34
+
35
+ def visualize_swapper_map
36
+ puts "swapper map"
37
+ 10.times do |index|
38
+ key = 1
39
+ puts ScatterSwap.swapper_map(index).join.to_s
40
+ end
41
+ end
42
+
43
+ def visualize_hash
44
+ puts "hash"
45
+ 40.times do |integer|
46
+ output = "|"
47
+ 3.times do |index|
48
+ output += " #{(integer + (123456789 * index)).to_s.rjust(5, ' ')}"
49
+ output += " => #{hashed = ScatterSwap.hash(integer + (123456789 * index) ) }"
50
+ output += " => #{ScatterSwap.reverse_hash(hashed) } |"
51
+ end
52
+ puts output
53
+ end
54
+ end
55
+
56
+
57
+ def visualize_scatter
58
+ puts "original scattered unscattered"
59
+ 20.times do |integer|
60
+ output = ""
61
+ 2.times do |index|
62
+ original = ScatterSwap.arrayify(integer + (123456789 * index))
63
+ scattered = ScatterSwap.scatter(original.clone)
64
+ unscattered = ScatterSwap.unscatter(scattered.clone)
65
+ output += "#{original.join}"
66
+ output += " => #{scattered.join}"
67
+ output += " => #{unscattered.join} | "
68
+ end
69
+ puts output
70
+ end
71
+ end
72
+
73
+
74
+ # find hash for lots of spins
75
+ def visualize_spin
76
+ 2000.times do |original|
77
+ hashed_values = []
78
+ 9000000000.times do |spin|
79
+ hashed = ScatterSwap.hash(original, spin)
80
+ if hashed_values.include? hashed
81
+ puts "collision: #{original} - #{spin} - #{hashed}"
82
+ break
83
+ end
84
+ hashed_values.push hashed
85
+ end
86
+ end
87
+ end
88
+
89
+ #visualize_spin
90
+ #visualize_hash
91
+ #visualize_scatter_and_unscatter
92
+ #visualize_scatter
93
+ #visualize_swapper_map
@@ -1,3 +1,3 @@
1
1
  module ScatterSwap
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
data/lib/scatter_swap.rb CHANGED
@@ -1,140 +1,12 @@
1
1
  require "scatter_swap/version"
2
+ require "scatter_swap/hasher"
2
3
 
3
- class ScatterSwap
4
- # This is the hashing function behind ObfuscateId.
5
- # https://github.com/namick/obfuscate_id
6
- #
7
- # Designing a hash function is a bit of a black art and
8
- # being that I don't have math background, I must resort
9
- # to this simplistic swaping and scattering of array elements.
10
- #
11
- # After writing this and reading/learning some elemental hashing techniques,
12
- # I realize this library is what is known as a Minimal perfect hash function:
13
- # http://en.wikipedia.org/wiki/Perfect_hash_function#Minimal_perfect_hash_function
14
- #
15
- # I welcome all improvements :-)
16
- #
17
- # If you have some comments or suggestions, please contact me on github
18
- # https://github.com/namick
19
- #
20
- # - nathan amick
21
- #
22
- #
23
- # This library is built for integers that can be expressed with 10 digits:
24
- # It zero pads smaller numbers... so the number one is expressed with:
25
- # 0000000001
26
- # The biggest number it can deal with is:
27
- # 9999999999
28
- #
29
- # Since we are working with a limited sequential set of input integers, 10 billion,
30
- # this algorithm will suffice for simple id obfuscation for many web apps.
31
- # The largest value that Ruby on Rails default id, Mysql INT type, is just over 2 billion (2147483647)
32
- # which is the same as 2 to the power of 31 minus 1, but considerably less than 10 billion.
33
- #
34
- # ScatterSwap is an integer hash function designed to have:
35
- # - zero collisions ( http://en.wikipedia.org/wiki/Perfect_hash_function )
36
- # - achieve avalanche ( http://en.wikipedia.org/wiki/Avalanche_effect )
37
- # - reversable
38
- #
39
- # We do that by combining two distinct strategies.
40
- #
41
- # 1. Scattering - whereby we take the whole number, slice it up into 10 digits
42
- # and rearange their places, yet retaining the same 10 digits. This allows
43
- # us to preserve the sum of all the digits, regardless of order. This sum acts
44
- # as a key to reverse this scattering effect.
45
- #
46
- # 2. Swapping - when dealing with many sequential numbers that we don't want
47
- # to look similar, scattering wont do us much good because so many of the
48
- # digits are the same; it deoesn't help to scatter 9 zeros around, so we need
49
- # to swap out each of those zeros for something else.. something different
50
- # for each place in the 10 digit array; for this, we need a map so that we
51
- # can reverse it.
52
-
53
- # Convience class method pointing to the instance method
4
+ module ScatterSwap
54
5
  def self.hash(plain_integer, spin = 0)
55
- new(plain_integer, spin).hash
6
+ Hasher.new(plain_integer, spin).hash
56
7
  end
57
8
 
58
- # Convience class method pointing to the instance method
59
9
  def self.reverse_hash(hashed_integer, spin = 0)
60
- new(hashed_integer, spin).reverse_hash
61
- end
62
-
63
- def initialize(original_integer, spin = 0)
64
- @original_integer = original_integer
65
- @spin = spin
66
- zero_pad = original_integer.to_s.rjust(10, '0')
67
- @working_array = zero_pad.split("").collect {|d| d.to_i}
68
- end
69
-
70
- attr_accessor :working_array
71
-
72
- # obfuscates an integer up to 10 digits in length
73
- def hash
74
- swap
75
- scatter
76
- completed_string
77
- end
78
-
79
- # de-obfuscates an integer
80
- def reverse_hash
81
- unscatter
82
- unswap
83
- completed_string
84
- end
85
-
86
- def completed_string
87
- @working_array.join
88
- end
89
-
90
- # We want a unique map for each place in the original number
91
- def swapper_map(index)
92
- array = (0..9).to_a
93
- 10.times.collect.with_index do |i|
94
- array.rotate!(index + i ^ spin).pop
95
- end
96
- end
97
-
98
- # Using a unique map for each of the ten places,
99
- # we swap out one number for another
100
- def swap
101
- @working_array = @working_array.collect.with_index do |digit, index|
102
- swapper_map(index)[digit]
103
- end
104
- end
105
-
106
- # Reverse swap
107
- def unswap
108
- @working_array = @working_array.collect.with_index do |digit, index|
109
- swapper_map(index).rindex(digit)
110
- end
111
- end
112
-
113
- # Rearrange the order of each digit in a reversable way by using the
114
- # sum of the digits (which doesn't change regardless of order)
115
- # as a key to record how they were scattered
116
- def scatter
117
- sum_of_digits = @working_array.inject(:+).to_i
118
- @working_array = 10.times.collect do
119
- @working_array.rotate!(spin ^ sum_of_digits).pop
120
- end
121
- end
122
-
123
- # Reverse the scatter
124
- def unscatter
125
- scattered_array = @working_array
126
- sum_of_digits = scattered_array.inject(:+).to_i
127
- @working_array = []
128
- @working_array.tap do |unscatter|
129
- 10.times do
130
- unscatter.push scattered_array.pop
131
- unscatter.rotate! (sum_of_digits ^ spin) * -1
132
- end
133
- end
134
- end
135
-
136
- # Add some spice so that different apps can have differently mapped hashes
137
- def spin
138
- @spin || 0
10
+ Hasher.new(hashed_integer, spin).reverse_hash
139
11
  end
140
12
  end
@@ -0,0 +1,87 @@
1
+ require 'spec_helper'
2
+ require 'scatter_swap'
3
+
4
+ describe "#hash" do
5
+ it "should be 10 digits" do
6
+ 100.times do |integer|
7
+ ScatterSwap.hash(integer).to_s.length.should == 10
8
+ end
9
+ end
10
+
11
+ it "should not be sequential" do
12
+ first = ScatterSwap.hash(1)
13
+ second = ScatterSwap.hash(2)
14
+ second.should_not eql(first.to_i + 1)
15
+ end
16
+
17
+ it "should be reversable" do
18
+ 100.times do |integer|
19
+ hashed = ScatterSwap.hash(integer)
20
+ ScatterSwap.reverse_hash(hashed).to_i.should == integer
21
+ end
22
+ end
23
+ end
24
+
25
+ describe "#swapper_map" do
26
+ before do
27
+ @map_set = []
28
+ s = ScatterSwap::Hasher.new(1)
29
+ 10.times do |digit|
30
+ @map_set.push s.swapper_map(digit)
31
+ end
32
+ end
33
+
34
+ it "should create a unique map array for each digit" do
35
+ @map_set.length.should == 10
36
+ @map_set.uniq.length.should == 10
37
+ end
38
+
39
+ it "should include all 10 digits in each map" do
40
+ @map_set.each do |map|
41
+ map.length.should == 10
42
+ map.uniq.length.should == 10
43
+ end
44
+ end
45
+ end
46
+
47
+ describe "#scatter" do
48
+ it "should return a number different from original" do
49
+ 100.times do |integer|
50
+ s = ScatterSwap::Hasher.new(integer)
51
+ original_array = s.working_array
52
+ s.scatter
53
+ s.working_array.should_not == original_array
54
+ end
55
+ end
56
+
57
+ it "should be reversable" do
58
+ 100.times do |integer|
59
+ s = ScatterSwap::Hasher.new(integer)
60
+ original_array = s.working_array.clone
61
+ s.scatter
62
+ s.unscatter
63
+ s.working_array.should == original_array
64
+ end
65
+ end
66
+ end
67
+
68
+ describe "#swap" do
69
+ it "should be different from original" do
70
+ 100.times do |integer|
71
+ s = ScatterSwap::Hasher.new(integer)
72
+ original_array = s.working_array.clone
73
+ s.swap
74
+ s.working_array.should_not == original_array
75
+ end
76
+ end
77
+
78
+ it "should be reversable" do
79
+ 100.times do |integer|
80
+ s = ScatterSwap::Hasher.new(integer)
81
+ original_array = s.working_array.clone
82
+ s.swap
83
+ s.unswap
84
+ s.working_array.should == original_array
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,17 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+ RSpec.configure do |config|
8
+ config.treat_symbols_as_metadata_keys_with_true_values = true
9
+ config.run_all_when_everything_filtered = true
10
+ config.filter_run :focus
11
+
12
+ # Run specs in random order to surface order dependencies. If you find an
13
+ # order dependency and want to debug it, you can fix the order by providing
14
+ # the seed, which is printed after each run.
15
+ # --seed 1234
16
+ config.order = 'random'
17
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scatter_swap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-03-27 00:00:00.000000000 Z
12
+ date: 2013-03-28 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: ScatterSwap is an integer hash function designed to have zero collisions,
15
15
  achieve avalanche, and be reversible.
@@ -20,13 +20,18 @@ extensions: []
20
20
  extra_rdoc_files: []
21
21
  files:
22
22
  - .gitignore
23
+ - .rspec
23
24
  - Gemfile
24
25
  - LICENSE.txt
25
26
  - README.md
26
27
  - Rakefile
27
28
  - lib/scatter_swap.rb
29
+ - lib/scatter_swap/hasher.rb
30
+ - lib/scatter_swap/run.rb
28
31
  - lib/scatter_swap/version.rb
29
32
  - scatter_swap.gemspec
33
+ - spec/scatter_swap_spec.rb
34
+ - spec/spec_helper.rb
30
35
  homepage: ''
31
36
  licenses: []
32
37
  post_install_message:
@@ -47,8 +52,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
47
52
  version: '0'
48
53
  requirements: []
49
54
  rubyforge_project:
50
- rubygems_version: 1.8.24
55
+ rubygems_version: 1.8.23
51
56
  signing_key:
52
57
  specification_version: 3
53
58
  summary: Minimal perfect hash function for 10 digit integers
54
- test_files: []
59
+ test_files:
60
+ - spec/scatter_swap_spec.rb
61
+ - spec/spec_helper.rb