I have a need to create tar archives deterministically and reproducibly. Since we already have similar functionality in mozpack for producting zip/jar archives, I figured it made sense for this functionality to live in mozpack. I made the functionality as simple as possible: we only accept files from the filesystem and the set of files must be known in advance. No class to hold/buffer state: just a simple function that takes a mapping of files and writes to a stream. MozReview-Commit-ID: If0NTcA7wpc
191 lines
5.6 KiB
Python
191 lines
5.6 KiB
Python
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
from __future__ import absolute_import
|
|
|
|
import hashlib
|
|
import os
|
|
import shutil
|
|
import stat
|
|
import tarfile
|
|
import tempfile
|
|
import unittest
|
|
|
|
from mozpack.archive import (
|
|
DEFAULT_MTIME,
|
|
create_tar_from_files,
|
|
create_tar_gz_from_files,
|
|
create_tar_bz2_from_files,
|
|
)
|
|
|
|
from mozunit import main
|
|
|
|
|
|
MODE_STANDARD = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
|
|
|
|
|
|
def file_hash(path):
|
|
h = hashlib.sha1()
|
|
with open(path, 'rb') as fh:
|
|
while True:
|
|
data = fh.read(8192)
|
|
if not data:
|
|
break
|
|
h.update(data)
|
|
|
|
return h.hexdigest()
|
|
|
|
|
|
class TestArchive(unittest.TestCase):
|
|
def _create_files(self, root):
|
|
files = {}
|
|
for i in range(10):
|
|
p = os.path.join(root, b'file%d' % i)
|
|
with open(p, 'wb') as fh:
|
|
fh.write(b'file%d' % i)
|
|
# Need to set permissions or umask may influence testing.
|
|
os.chmod(p, MODE_STANDARD)
|
|
files[b'file%d' % i] = p
|
|
|
|
return files
|
|
|
|
def _verify_basic_tarfile(self, tf):
|
|
self.assertEqual(len(tf.getmembers()), 10)
|
|
|
|
names = ['file%d' % i for i in range(10)]
|
|
self.assertEqual(tf.getnames(), names)
|
|
|
|
for ti in tf.getmembers():
|
|
self.assertEqual(ti.uid, 0)
|
|
self.assertEqual(ti.gid, 0)
|
|
self.assertEqual(ti.uname, '')
|
|
self.assertEqual(ti.gname, '')
|
|
self.assertEqual(ti.mode, MODE_STANDARD)
|
|
self.assertEqual(ti.mtime, DEFAULT_MTIME)
|
|
|
|
def test_dirs_refused(self):
|
|
d = tempfile.mkdtemp()
|
|
try:
|
|
tp = os.path.join(d, 'test.tar')
|
|
with open(tp, 'wb') as fh:
|
|
with self.assertRaisesRegexp(ValueError, 'not a regular'):
|
|
create_tar_from_files(fh, {'test': d})
|
|
finally:
|
|
shutil.rmtree(d)
|
|
|
|
def test_setuid_setgid_refused(self):
|
|
d = tempfile.mkdtemp()
|
|
try:
|
|
uid = os.path.join(d, 'setuid')
|
|
gid = os.path.join(d, 'setgid')
|
|
with open(uid, 'a'):
|
|
pass
|
|
with open(gid, 'a'):
|
|
pass
|
|
|
|
os.chmod(uid, MODE_STANDARD | stat.S_ISUID)
|
|
os.chmod(gid, MODE_STANDARD | stat.S_ISGID)
|
|
|
|
tp = os.path.join(d, 'test.tar')
|
|
with open(tp, 'wb') as fh:
|
|
with self.assertRaisesRegexp(ValueError, 'cannot add file with setuid'):
|
|
create_tar_from_files(fh, {'test': uid})
|
|
with self.assertRaisesRegexp(ValueError, 'cannot add file with setuid'):
|
|
create_tar_from_files(fh, {'test': gid})
|
|
finally:
|
|
shutil.rmtree(d)
|
|
|
|
def test_create_tar_basic(self):
|
|
d = tempfile.mkdtemp()
|
|
try:
|
|
files = self._create_files(d)
|
|
|
|
tp = os.path.join(d, 'test.tar')
|
|
with open(tp, 'wb') as fh:
|
|
create_tar_from_files(fh, files)
|
|
|
|
# Output should be deterministic.
|
|
self.assertEqual(file_hash(tp), 'cd16cee6f13391abd94dfa435d2633b61ed727f1')
|
|
|
|
with tarfile.open(tp, 'r') as tf:
|
|
self._verify_basic_tarfile(tf)
|
|
|
|
finally:
|
|
shutil.rmtree(d)
|
|
|
|
def test_executable_preserved(self):
|
|
d = tempfile.mkdtemp()
|
|
try:
|
|
p = os.path.join(d, 'exec')
|
|
with open(p, 'wb') as fh:
|
|
fh.write('#!/bin/bash\n')
|
|
os.chmod(p, MODE_STANDARD | stat.S_IXUSR)
|
|
|
|
tp = os.path.join(d, 'test.tar')
|
|
with open(tp, 'wb') as fh:
|
|
create_tar_from_files(fh, {'exec': p})
|
|
|
|
self.assertEqual(file_hash(tp), '357e1b81c0b6cfdfa5d2d118d420025c3c76ee93')
|
|
|
|
with tarfile.open(tp, 'r') as tf:
|
|
m = tf.getmember('exec')
|
|
self.assertEqual(m.mode, MODE_STANDARD | stat.S_IXUSR)
|
|
|
|
finally:
|
|
shutil.rmtree(d)
|
|
|
|
def test_create_tar_gz_basic(self):
|
|
d = tempfile.mkdtemp()
|
|
try:
|
|
files = self._create_files(d)
|
|
|
|
gp = os.path.join(d, 'test.tar.gz')
|
|
with open(gp, 'wb') as fh:
|
|
create_tar_gz_from_files(fh, files)
|
|
|
|
self.assertEqual(file_hash(gp), 'acb602239c1aeb625da5e69336775609516d60f5')
|
|
|
|
with tarfile.open(gp, 'r:gz') as tf:
|
|
self._verify_basic_tarfile(tf)
|
|
|
|
finally:
|
|
shutil.rmtree(d)
|
|
|
|
def test_tar_gz_name(self):
|
|
d = tempfile.mkdtemp()
|
|
try:
|
|
files = self._create_files(d)
|
|
|
|
gp = os.path.join(d, 'test.tar.gz')
|
|
with open(gp, 'wb') as fh:
|
|
create_tar_gz_from_files(fh, files, filename='foobar', compresslevel=1)
|
|
|
|
self.assertEqual(file_hash(gp), 'fd099f96480cc1100f37baa8e89a6b820dbbcbd3')
|
|
|
|
with tarfile.open(gp, 'r:gz') as tf:
|
|
self._verify_basic_tarfile(tf)
|
|
|
|
finally:
|
|
shutil.rmtree(d)
|
|
|
|
def test_create_tar_bz2_basic(self):
|
|
d = tempfile.mkdtemp()
|
|
try:
|
|
files = self._create_files(d)
|
|
|
|
bp = os.path.join(d, 'test.tar.bz2')
|
|
with open(bp, 'wb') as fh:
|
|
create_tar_bz2_from_files(fh, files)
|
|
|
|
self.assertEqual(file_hash(bp), '1827ad00dfe7acf857b7a1c95ce100361e3f6eea')
|
|
|
|
with tarfile.open(bp, 'r:bz2') as tf:
|
|
self._verify_basic_tarfile(tf)
|
|
finally:
|
|
shutil.rmtree(d)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|