tarwriter.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. #
  2. # The Qubes OS Project, http://www.qubes-os.org
  3. #
  4. # Copyright (C) 2016 Marek Marczykowski-Górecki
  5. # <marmarek@invisiblethingslab.com>
  6. #
  7. # This program is free software; you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation; either version 2 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License along
  18. # with this program; if not, write to the Free Software Foundation, Inc.,
  19. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  20. import argparse
  21. import functools
  22. import subprocess
  23. import tarfile
  24. import io
  25. BUF_SIZE = 409600
  26. class TarSparseInfo(tarfile.TarInfo):
  27. def __init__(self, name="", sparsemap=None):
  28. super(TarSparseInfo, self).__init__(name)
  29. if sparsemap is not None:
  30. self.type = tarfile.GNUTYPE_SPARSE
  31. self.sparsemap = list(sparsemap)
  32. # compact size
  33. self.size = functools.reduce(lambda x, y: x+y[1], sparsemap, 0)
  34. else:
  35. self.sparsemap = []
  36. @property
  37. def realsize(self):
  38. if self.sparsemap:
  39. return self.sparsemap[-1][0] + self.sparsemap[-1][1]
  40. return self.size
  41. def sparse_header_chunk(self, index):
  42. if index < len(self.sparsemap):
  43. return b''.join([
  44. tarfile.itn(self.sparsemap[index][0], 12, tarfile.GNU_FORMAT),
  45. tarfile.itn(self.sparsemap[index][1], 12, tarfile.GNU_FORMAT),
  46. ])
  47. return b'\0' * 12 * 2
  48. def get_gnu_header(self):
  49. '''Part placed in 'prefix' field of posix header'''
  50. parts = [
  51. tarfile.itn(self.mtime, 12, tarfile.GNU_FORMAT), # atime
  52. tarfile.itn(self.mtime, 12, tarfile.GNU_FORMAT), # ctime
  53. tarfile.itn(0, 12, tarfile.GNU_FORMAT), # offset
  54. tarfile.stn('', 4, tarfile.ENCODING, 'surrogateescape'), #longnames
  55. b'\0', # unused_pad2
  56. ]
  57. parts += [self.sparse_header_chunk(i) for i in range(4)]
  58. parts += [
  59. b'\1' if len(self.sparsemap) > 4 else b'\0', # isextended
  60. tarfile.itn(self.realsize, 12, tarfile.GNU_FORMAT), # realsize
  61. ]
  62. return b''.join(parts)
  63. def get_info(self):
  64. info = super(TarSparseInfo, self).get_info()
  65. # place GNU extension into
  66. info['prefix'] = self.get_gnu_header().decode(tarfile.ENCODING)
  67. return info
  68. def tobuf(self, format=tarfile.DEFAULT_FORMAT, encoding=tarfile.ENCODING,
  69. errors="strict"):
  70. # pylint: disable=redefined-builtin
  71. header_buf = super(TarSparseInfo, self).tobuf(format, encoding, errors)
  72. if len(self.sparsemap) > 4:
  73. return header_buf + b''.join(self.create_ext_sparse_headers())
  74. return header_buf
  75. def create_ext_sparse_headers(self):
  76. for ext_hdr in range(4, len(self.sparsemap), 21):
  77. sparse_parts = [
  78. self.sparse_header_chunk(i).decode(
  79. tarfile.ENCODING, 'surrogateescape')
  80. for i in range(ext_hdr, ext_hdr+21)]
  81. sparse_parts.append(
  82. '\1' if ext_hdr+21 < len(self.sparsemap) else '\0')
  83. yield tarfile.stn(''.join(sparse_parts), 512,
  84. tarfile.ENCODING, 'surrogateescape')
  85. def get_sparse_map(input_file):
  86. '''
  87. Return map of the file where actual data is present, ignoring zero-ed
  88. blocks. Last entry of the map spans to the end of file, even if that part is
  89. zero-size (when file ends with zeros).
  90. This function is performance critical.
  91. :param input_file: io.File object
  92. :return: iterable of (offset, size)
  93. '''
  94. zero_block = bytearray(tarfile.BLOCKSIZE)
  95. buf = bytearray(BUF_SIZE)
  96. in_data_block = False
  97. data_block_start = 0
  98. buf_start_offset = 0
  99. while True:
  100. buf_len = input_file.readinto(buf)
  101. if not buf_len:
  102. break
  103. for offset in range(0, buf_len, tarfile.BLOCKSIZE):
  104. if buf[offset:offset+tarfile.BLOCKSIZE] == zero_block:
  105. if in_data_block:
  106. in_data_block = False
  107. yield (data_block_start,
  108. buf_start_offset+offset-data_block_start)
  109. else:
  110. if not in_data_block:
  111. in_data_block = True
  112. data_block_start = buf_start_offset+offset
  113. buf_start_offset += buf_len
  114. if in_data_block:
  115. yield (data_block_start, buf_start_offset-data_block_start)
  116. else:
  117. # always emit last slice to the input end - otherwise extracted file
  118. # will be truncated
  119. yield (buf_start_offset, 0)
  120. def copy_sparse_data(input_stream, output_stream, sparse_map):
  121. '''Copy data blocks from input to output according to sparse_map
  122. :param input_stream: io.IOBase input instance
  123. :param output_stream: io.IOBase output instance
  124. :param sparse_map: iterable of (offset, size)
  125. '''
  126. buf = bytearray(BUF_SIZE)
  127. for chunk in sparse_map:
  128. input_stream.seek(chunk[0])
  129. left = chunk[1]
  130. while left:
  131. if left > BUF_SIZE:
  132. read = input_stream.readinto(buf)
  133. output_stream.write(buf[:read])
  134. else:
  135. buf_trailer = input_stream.read(left)
  136. read = len(buf_trailer)
  137. output_stream.write(buf_trailer)
  138. left -= read
  139. if not read:
  140. raise Exception('premature EOF')
  141. def finalize(output):
  142. '''Write EOF blocks'''
  143. output.write(b'\0' * 512)
  144. output.write(b'\0' * 512)
  145. def main(args=None):
  146. parser = argparse.ArgumentParser()
  147. parser.add_argument('--override-name', action='store', dest='override_name',
  148. help='use this name in tar header')
  149. parser.add_argument('--use-compress-program', default=None,
  150. metavar='COMMAND', action='store', dest='use_compress_program',
  151. help='Filter data through COMMAND.')
  152. parser.add_argument('input_file',
  153. help='input file name')
  154. parser.add_argument('output_file', default='-', nargs='?',
  155. help='output file name')
  156. args = parser.parse_args(args)
  157. input_file = io.open(args.input_file, 'rb')
  158. sparse_map = list(get_sparse_map(input_file))
  159. header_name = args.input_file
  160. if args.override_name:
  161. header_name = args.override_name
  162. tar_info = TarSparseInfo(header_name, sparse_map)
  163. if args.output_file == '-':
  164. output = io.open('/dev/stdout', 'wb')
  165. else:
  166. output = io.open(args.output_file, 'wb')
  167. if args.use_compress_program:
  168. compress = subprocess.Popen([args.use_compress_program],
  169. stdin=subprocess.PIPE, stdout=output)
  170. output = compress.stdin
  171. else:
  172. compress = None
  173. output.write(tar_info.tobuf(tarfile.GNU_FORMAT))
  174. copy_sparse_data(input_file, output, sparse_map)
  175. finalize(output)
  176. input_file.close()
  177. output.close()
  178. if compress is not None:
  179. compress.wait()
  180. return compress.returncode
  181. return 0
  182. if __name__ == '__main__':
  183. main()