web-dev-qa-db-fra.com

Création d'un hachage md5 d'un nombre, d'une chaîne, d'un tableau ou d'un hachage dans Ruby

J'ai besoin de créer une chaîne de signature pour une variable dans Ruby, où la variable peut être un nombre, une chaîne, un hachage ou un tableau. Les valeurs de hachage et les éléments de tableau peuvent également être de n'importe lequel de ces types.

Cette chaîne sera utilisée pour comparer les valeurs dans une base de données (Mongo, dans ce cas).

Ma première pensée a été de créer un hachage MD5 d'une valeur codée JSON, comme ceci: (le corps est la variable mentionnée ci-dessus)

def createsig(body)    
  Digest::MD5.hexdigest(JSON.generate(body))
end

Cela fonctionne presque, mais JSON.generate n'encode pas les clés d'un hachage dans le même ordre à chaque fois, donc createsig({:a=>'a',:b=>'b'}) n'est pas toujours égal à createsig({:b=>'b',:a=>'a'}).

Quelle est la meilleure façon de créer une chaîne de signature pour répondre à ce besoin?

Remarque: Pour le détail orienté parmi nous, je sais que vous ne pouvez pas JSON.generate() un nombre ou une chaîne. Dans ces cas, j'appellerais simplement MD5.hexdigest() directement.

40
TelegramSam

Je coder les éléments suivants assez rapidement et je n'ai pas le temps de vraiment le tester ici au travail, mais il devrait faire le travail. Faites-moi savoir si vous rencontrez des problèmes et je vais y jeter un œil.

Cela devrait aplatir et trier correctement les tableaux et les hachages, et vous devrez avoir des chaînes d'aspect assez étrange pour qu'il y ait des collisions.

def createsig(body)
  Digest::MD5.hexdigest( sigflat body )
end

def sigflat(body)
  if body.class == Hash
    arr = []
    body.each do |key, value|
      arr << "#{sigflat key}=>#{sigflat value}"
    end
    body = arr
  end
  if body.class == Array
    str = ''
    body.map! do |value|
      sigflat value
    end.sort!.each do |value|
      str << value
    end
  end
  if body.class != String
    body = body.to_s << body.class.to_s
  end
  body
end

> sigflat({:a => {:b => 'b', :c => 'c'}, :d => 'd'}) == sigflat({:d => 'd', :a => {:c => 'c', :b => 'b'}})
=> true
31
Luke

Si vous ne pouviez obtenir qu'une représentation sous forme de chaîne de body et que le hachage Ruby 1.8 revenait avec des ordres différents d'une fois à l'autre, vous pourriez hacher de manière fiable cette représentation de chaîne Saluons les mains avec quelques patchs de singe:

require 'digest/md5'

class Object
  def md5key
    to_s
  end
end

class Array
  def md5key
    map(&:md5key).join
  end
end

class Hash
  def md5key
    sort.map(&:md5key).join
  end
end

Maintenant, tout objet (du type mentionné dans la question) répond à md5key en renvoyant une clé fiable à utiliser pour créer une somme de contrôle, donc:

def createsig(o)
  Digest::MD5.hexdigest(o.md5key)
end

Exemple:

body = [
  {
    'bar' => [
      345,
      "baz",
    ],
    'qux' => 7,
  },
  "foo",
  123,
]
p body.md5key        # => "bar345bazqux7foo123"
p createsig(body)    # => "3a92036374de88118faf19483fe2572e"

Remarque: Cette représentation de hachage ne code pas la structure, seulement la concaténation des valeurs. Par conséquent, ["a", "b", "c"] hachera la même chose que ["abc"].

13
Wayne Conrad

Voici ma solution. Je parcours la structure de données et crée une liste de pièces qui sont jointes en une seule chaîne. Afin de garantir que les types de classe vus affectent le hachage, j'injecte un seul caractère unicode qui code les informations de type de base en cours de route. (Par exemple, nous voulons ["1", "2", "3"]. Objsum! = [1,2,3] .objsum)

J'ai fait cela comme un raffinement sur Object, il est facilement porté sur un patch de singe. Pour l'utiliser, il suffit de disposer du fichier et de lancer "using ObjSum".

module ObjSum
  refine Object do
    def objsum
      parts = []
      queue = [self]

      while queue.size > 0
        item = queue.shift

        if item.kind_of?(Hash)
          parts << "\\000"
          item.keys.sort.each do |k| 
            queue << k
            queue << item[k]
          end
        elsif item.kind_of?(Set)
          parts << "\\001"
          item.to_a.sort.each { |i| queue << i }
        elsif item.kind_of?(Enumerable)
          parts << "\\002"
          item.each { |i| queue << i }
        elsif item.kind_of?(Fixnum)
          parts << "\\003"
          parts << item.to_s
        elsif item.kind_of?(Float)
          parts << "\\004"
          parts << item.to_s
        else
          parts << item.to_s
        end
      end

      Digest::MD5.hexdigest(parts.join)
    end
  end
end
1
Greg Fodor

Juste mes 2 cents:

module Ext
  module Hash
    module InstanceMethods
      # Return a string suitable for generating content signature.
      # Signature image does not depend on order of keys.
      #
      #   {:a => 1, :b => 2}.signature_image == {:b => 2, :a => 1}.signature_image                  # => true
      #   {{:a => 1, :b => 2} => 3}.signature_image == {{:b => 2, :a => 1} => 3}.signature_image    # => true
      #   etc.
      #
      # NOTE: Signature images of identical content generated under different versions of Ruby are NOT GUARANTEED to be identical.
      def signature_image
        # Store normalized key-value pairs here.
        ar = []

        each do |k, v|
          ar << [
            k.is_a?(::Hash) ? k.signature_image : [k.class.to_s, k.inspect].join(":"),
            v.is_a?(::Hash) ? v.signature_image : [v.class.to_s, v.inspect].join(":"),
          ]
        end

        ar.sort.inspect
      end
    end
  end
end

class Hash    #:nodoc:
  include Ext::Hash::InstanceMethods
end
1
Alex Fortuna