Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modify checksum logic to use system binaries #251

Merged
merged 11 commits into from
Aug 10, 2018
97 changes: 78 additions & 19 deletions lib/train/file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@

require 'train/file/local'
require 'train/file/remote'
require 'digest/sha2'
require 'digest/md5'
require 'train/extras/stat'

module Train
class File
class File # rubocop:disable Metrics/ClassLength
def initialize(backend, path, follow_symlink = true)
@backend = backend
@path = path || ''
Expand Down Expand Up @@ -48,22 +46,6 @@ def type
:unknown
end

def md5sum
res = Digest::MD5.new
res.update(content)
res.hexdigest
rescue TypeError => _
nil
end

def sha256sum
res = Digest::SHA256.new
res.update(content)
res.hexdigest
rescue TypeError => _
nil
end

def source
if @follow_symlink
self.class.new(@backend, @path, false)
Expand Down Expand Up @@ -147,5 +129,82 @@ def mounted?

!mounted.nil? && !mounted.stdout.nil? && !mounted.stdout.empty?
end

def md5sum
# Skip processing rest of method if fallback method is selected
return perform_checksum_ruby(:md5) if defined?(@ruby_checksum_fallback)

checksum = if @backend.os.family == 'windows'
perform_checksum_windows(:md5)
else
@md5_command ||= case @backend.os.family
when 'darwin'
'md5 -r'
when 'solaris'
'digest -a md5'
else
'md5sum'
end

perform_checksum_unix(@md5_command)
end

checksum || perform_checksum_ruby(:md5)
end

def sha256sum
# Skip processing rest of method if fallback method is selected
return perform_checksum_ruby(:sha256) if defined?(@ruby_checksum_fallback)

checksum = if @backend.os.family == 'windows'
perform_checksum_windows(:sha256)
else
@sha256_command ||= case @backend.os.family
when 'darwin', 'hpux', 'qnx'
'shasum -a 256'
when 'solaris'
'digest -a sha256'
else
'sha256sum'
end

perform_checksum_unix(@sha256_command)
end

checksum || perform_checksum_ruby(:sha256)
end

private

def perform_checksum_unix(cmd)
res = @backend.run_command("#{cmd} #{@path}")
res.stdout.split(' ').first if res.exit_status == 0
end

def perform_checksum_windows(method)
cmd = "CertUtil -hashfile #{@path} #{method.to_s.upcase}"
res = @backend.run_command(cmd)
res.stdout.split("\r\n")[1].tr(' ', '') if res.exit_status == 0
end

# This pulls the content of the file to the machine running Train and uses
# Digest to perform the checksum. This is less efficient than using remote
# system binaries and can lead to incorrect results due to encoding.
def perform_checksum_ruby(method)
# This is used to skip attempting other checksum methods. If this is set
# then we know all other methods have failed.
@ruby_checksum_fallback = true
case method
when :md5
res = Digest::MD5.new
when :sha256
res = Digest::SHA256.new
end

res.update(content)
res.hexdigest
rescue TypeError => _
nil
end
end
end
4 changes: 2 additions & 2 deletions test/integration/tests/path_block_device_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@
file.link_path.must_be_nil
end

it 'has no md5sum' do
it 'has the correct md5sum' do
file.md5sum.must_equal('d41d8cd98f00b204e9800998ecf8427e')
end

it 'has no sha256sum' do
it 'has the correct sha256sum' do
file.sha256sum.must_equal('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855')
end

Expand Down
8 changes: 4 additions & 4 deletions test/integration/tests/path_folder_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@
file.content.must_be_nil
end

it 'has an md5sum' do
file.md5sum.must_be_nil
it 'raises an error if md5sum is attempted' do
proc { file.md5sum }.must_raise RuntimeError
end

it 'has an sha256sum' do
file.sha256sum.must_be_nil
it 'raises an error if sha256sum is attempted' do
proc { file.sha256sum }.must_raise RuntimeError
end
end

Expand Down
8 changes: 4 additions & 4 deletions test/integration/tests/path_missing_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@
file.link_path.must_be_nil
end

it 'has an md5sum' do
file.md5sum.must_be_nil
it 'raises an error if md5sum is attempted' do
proc { file.md5sum }.must_raise RuntimeError
end

it 'has an sha256sum' do
file.sha256sum.must_be_nil
it 'raises an error if sha256sum is attempted' do
proc { file.sha256sum }.must_raise RuntimeError
end

it 'has a modified time' do
Expand Down
15 changes: 0 additions & 15 deletions test/integration/tests/path_pipe_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,6 @@
file.type.must_equal(:pipe)
end

# # TODO add back content
# it 'has no content' do
# file.content.must_be_nil
# end

it 'has owner name root' do
file.owner.must_equal('root')
end
Expand All @@ -43,16 +38,6 @@
file.link_path.must_be_nil
end

# # TODO add back content
# it 'has no md5sum' do
# file.md5sum.must_be_nil
# end
#
# # TODO add back content
# it 'has no sha256sum' do
# file.sha256sum.must_be_nil
# end

it 'has a modified time' do
file.mtime.must_be_close_to(Time.now.to_i - Test.mtime/2, Test.mtime)
end
Expand Down
80 changes: 79 additions & 1 deletion test/unit/file/local/unix_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,82 @@ def meta_stub(method, param, &block)
file_tester.unix_mode_mask('all', 'r').must_equal 0444
end
end
end

describe '#md5sum' do
let(:md5_checksum) { '57d4c6f9d15313fd5651317e588c035d' }

let(:ruby_md5_mock) do
checksum_mock = mock
checksum_mock.expects(:update).returns('')
checksum_mock.expects(:hexdigest).returns(md5_checksum)
checksum_mock
end

it 'defaults to a Ruby based checksum if other methods fail' do
backend.mock_command('md5sum /tmp/testfile', '', '', 1)
Digest::MD5.expects(:new).returns(ruby_md5_mock)
cls.new(backend, '/tmp/testfile').md5sum.must_equal md5_checksum
end

it 'calculates the correct md5sum on the `linux` platform family' do
output = "#{md5_checksum} /tmp/testfile"
backend.mock_command('md5sum /tmp/testfile', output)
cls.new(backend, '/tmp/testfile').md5sum.must_equal md5_checksum
end

it 'calculates the correct md5sum on the `darwin` platform family' do
output = "#{md5_checksum} /tmp/testfile"
backend.mock_os(family: 'darwin')
backend.mock_command('md5 -r /tmp/testfile', output)
cls.new(backend, '/tmp/testfile').md5sum.must_equal md5_checksum
end

it 'calculates the correct md5sum on the `solaris` platform family' do
# The `digest` command doesn't output the filename by default
output = "#{md5_checksum}"
backend.mock_os(family: 'solaris')
backend.mock_command('digest -a md5 /tmp/testfile', output)
cls.new(backend, '/tmp/testfile').md5sum.must_equal md5_checksum
end
end

describe '#sha256sum' do
let(:sha256_checksum) {
'491260aaa6638d4a64c714a17828c3d82bad6ca600c9149b3b3350e91bcd283d'
}

let(:ruby_sha256_mock) do
checksum_mock = mock
checksum_mock.expects(:update).returns('')
checksum_mock.expects(:hexdigest).returns(sha256_checksum)
checksum_mock
end

it 'defaults to a Ruby based checksum if other methods fail' do
backend.mock_command('sha256sum /tmp/testfile', '', '', 1)
Digest::SHA256.expects(:new).returns(ruby_sha256_mock)
cls.new(backend, '/tmp/testfile').sha256sum.must_equal sha256_checksum
end

it 'calculates the correct sha256sum on the `linux` platform family' do
output = "#{sha256_checksum} /tmp/testfile"
backend.mock_command('sha256sum /tmp/testfile', output)
cls.new(backend, '/tmp/testfile').sha256sum.must_equal sha256_checksum
end

it 'calculates the correct sha256sum on the `darwin` platform family' do
output = "#{sha256_checksum} /tmp/testfile"
backend.mock_os(family: 'darwin')
backend.mock_command('shasum -a 256 /tmp/testfile', output)
cls.new(backend, '/tmp/testfile').sha256sum.must_equal sha256_checksum
end

it 'calculates the correct sha256sum on the `solaris` platform family' do
# The `digest` command doesn't output the filename by default
output = "#{sha256_checksum}"
backend.mock_os(family: 'solaris')
backend.mock_command('digest -a sha256 /tmp/testfile', output)
cls.new(backend, '/tmp/testfile').sha256sum.must_equal sha256_checksum
end
end
end
60 changes: 60 additions & 0 deletions test/unit/file/local/windows_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,64 @@
backend.mock_command('Get-Acl "path" | select -expand Owner', out)
cls.new(backend, 'path').owner.must_equal out
end

describe '#md5sum' do
let(:md5_checksum) { '4ce0c733cdcf1d2f78532bbd9ce3441d' }
let(:filepath) { 'C:\Windows\explorer.exe' }

let(:ruby_md5_mock) do
checksum_mock = mock
checksum_mock.expects(:update).returns('')
checksum_mock.expects(:hexdigest).returns(md5_checksum)
checksum_mock
end

it 'defaults to a Ruby based checksum if other methods fail' do
backend.mock_command("CertUtil -hashfile #{filepath} MD5", '', '', 1)
Digest::MD5.expects(:new).returns(ruby_md5_mock)
cls.new(backend, '/tmp/testfile').md5sum.must_equal md5_checksum
end

it 'calculates the correct md5sum on the `windows` platform family' do
output = <<-EOC
MD5 hash of file C:\\Windows\\explorer.exe:\r
4c e0 c7 33 cd cf 1d 2f 78 53 2b bd 9c e3 44 1d\r
CertUtil: -hashfile command completed successfully.\r
EOC

backend.mock_command("CertUtil -hashfile #{filepath} MD5", output)
cls.new(backend, filepath).md5sum.must_equal md5_checksum
end
end

describe '#sha256sum' do
let(:sha256_checksum) {
'85270240a5fd51934f0627c92b2282749d071fdc9ac351b81039ced5b10f798b'
}
let(:filepath) { 'C:\Windows\explorer.exe' }

let(:ruby_sha256_mock) do
checksum_mock = mock
checksum_mock.expects(:update).returns('')
checksum_mock.expects(:hexdigest).returns(sha256_checksum)
checksum_mock
end

it 'defaults to a Ruby based checksum if other methods fail' do
backend.mock_command('CertUtil -hashfile #{filepath} SHA256', '', '', 1)
Digest::SHA256.expects(:new).returns(ruby_sha256_mock)
cls.new(backend, '/tmp/testfile').sha256sum.must_equal sha256_checksum
end

it 'calculates the correct sha256sum on the `windows` platform family' do
output = <<-EOC
SHA256 hash of file C:\\Windows\\explorer.exe:\r
85 27 02 40 a5 fd 51 93 4f 06 27 c9 2b 22 82 74 9d 07 1f dc 9a c3 51 b8 10 39 ce d5 b1 0f 79 8b\r
CertUtil: -hashfile command completed successfully.\r
EOC

backend.mock_command("CertUtil -hashfile #{filepath} SHA256", output)
cls.new(backend, filepath).sha256sum.must_equal sha256_checksum
end
end
end
48 changes: 48 additions & 0 deletions test/unit/file/remote/aix_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,52 @@
backend.mock_command("perl -e 'print readlink shift' path", 'our_link_path')
file.link_path.must_equal 'our_link_path'
end

describe '#md5sum' do
let(:md5_checksum) { '57d4c6f9d15313fd5651317e588c035d' }

let(:ruby_md5_mock) do
checksum_mock = mock
checksum_mock.expects(:update).returns('')
checksum_mock.expects(:hexdigest).returns(md5_checksum)
checksum_mock
end

it 'defaults to a Ruby based checksum if other methods fail' do
backend.mock_command('md5sum /tmp/testfile', '', '', 1)
Digest::MD5.expects(:new).returns(ruby_md5_mock)
cls.new(backend, '/tmp/testfile').md5sum.must_equal md5_checksum
end

it 'calculates the correct md5sum on the `aix` platform family' do
output = "#{md5_checksum} /tmp/testfile"
backend.mock_command('md5sum /tmp/testfile', output)
cls.new(backend, '/tmp/testfile').md5sum.must_equal md5_checksum
end
end

describe '#sha256sum' do
let(:sha256_checksum) {
'491260aaa6638d4a64c714a17828c3d82bad6ca600c9149b3b3350e91bcd283d'
}

let(:ruby_sha256_mock) do
checksum_mock = mock
checksum_mock.expects(:update).returns('')
checksum_mock.expects(:hexdigest).returns(sha256_checksum)
checksum_mock
end

it 'defaults to a Ruby based checksum if other methods fail' do
backend.mock_command('sha256sum /tmp/testfile', '', '', 1)
Digest::SHA256.expects(:new).returns(ruby_sha256_mock)
cls.new(backend, '/tmp/testfile').sha256sum.must_equal sha256_checksum
end

it 'calculates the correct sha256sum on the `aix` platform family' do
output = "#{sha256_checksum} /tmp/testfile"
backend.mock_command('sha256sum /tmp/testfile', output)
cls.new(backend, '/tmp/testfile').sha256sum.must_equal sha256_checksum
end
end
end
Loading