Crear un hash md5 de un número, cadena, matriz o hash en Ruby
digest (5)
Necesito crear una cadena de firma para una variable en Ruby, donde la variable puede ser un número, una cadena, un hash o una matriz. Los valores de hash y los elementos de matriz también pueden ser cualquiera de estos tipos.
Esta cadena se usará para comparar los valores en una base de datos (Mongo, en este caso).
Lo primero que pensé fue crear un valor hash MD5 de un valor codificado JSON, así: (body es la variable mencionada anteriormente)
def createsig(body)
Digest::MD5.hexdigest(JSON.generate(body))
end
Esto casi funciona, pero JSON.generate no codifica las claves de un hash en el mismo orden cada vez, por lo que createsig({:a=>''a'',:b=>''b''})
no siempre es igual a createsig({:b=>''b'',:a=>''a''})
.
¿Cuál es la mejor manera de crear una cadena de firma que se ajuste a esta necesidad?
Nota: para el detalle orientado entre nosotros, sé que no puede JSON.generate()
un número o una cadena. En estos casos, simplemente llamaría a MD5.hexdigest()
directamente.
Aquí está mi solución. Camino por la estructura de datos y construyo una lista de piezas que se unen en una sola cadena. Para garantizar que los tipos de clases observados afecten al hash, inyecto un único carácter Unicode que codifica información de tipo básico a lo largo del camino. (Por ejemplo, queremos ["1", "2", "3"]. Objsum! = [1,2,3] .objsum)
Lo hice como un refinamiento en Object, es fácilmente portado a un parche de mono. Para usarlo solo necesita el archivo y ejecuta "usando 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
Dependiendo de tus necesidades, puedes llamar a ary.inspect
o ary.to_yaml
, incluso.
Estoy codificando lo siguiente bastante rápido y no tengo tiempo para probarlo realmente en el trabajo, pero debería hacer el trabajo. Avíseme si encuentra algún problema y lo revisaré.
Esto debería aplanarse y ordenar las matrices y los hash, y tendrías que tener algunas cuerdas bastante extrañas para que haya colisiones.
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
Si solo pudieras obtener una representación de cuerdas del body
y no hacer que el hash Ruby 1.8 volviera con diferentes órdenes de una vez a la otra, podrías hash confiablemente esa representación de cadena. Vamos a ensuciarnos las manos con algunos parches de mono:
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
Ahora cualquier objeto (de los tipos mencionados en la pregunta) responde a md5key
devolviendo una clave confiable para usarla para crear una suma de comprobación, entonces:
def createsig(o)
Digest::MD5.hexdigest(o.md5key)
end
Ejemplo:
body = [
{
''bar'' => [
345,
"baz",
],
''qux'' => 7,
},
"foo",
123,
]
p body.md5key # => "bar345bazqux7foo123"
p createsig(body) # => "3a92036374de88118faf19483fe2572e"
Nota: Esta representación hash no codifica la estructura, solo la concatenación de los valores. Por lo tanto, ["a", "b", "c"] marcará lo mismo que ["abc"].
Solo mis 2 centavos:
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