main.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. #!/usr/bin/env python3
  2. # vim: sw=4 ts=4 et tw=100 cc=+1
  3. #
  4. ####################################################################################################
  5. # DESCRIPTION #
  6. ####################################################################################################
  7. #
  8. # Decompressor/compressor for files in Mozilla's "mozLz4" format. Firefox uses this file format to
  9. # compress e. g. bookmark backups (*.jsonlz4).
  10. #
  11. # This file format is in fact just plain LZ4 data with a custom header (magic number [8 bytes] and
  12. # uncompressed file size [4 bytes, little endian]).
  13. #
  14. ####################################################################################################
  15. # DEPENDENCIES #
  16. ####################################################################################################
  17. #
  18. # - Tested with Python 3.10
  19. # - LZ4 bindings for Python, version 4.x: https://pypi.python.org/pypi/lz4
  20. #
  21. ####################################################################################################
  22. # LICENSE #
  23. ####################################################################################################
  24. #
  25. # Copyright (c) 2015-2022, Tilman Blumenbach
  26. # All rights reserved.
  27. #
  28. # Redistribution and use in source and binary forms, with or without modification, are permitted
  29. # provided that the following conditions are met:
  30. #
  31. # 1. Redistributions of source code must retain the above copyright notice, this list of conditions
  32. # and the following disclaimer.
  33. # 2. Redistributions in binary form must reproduce the above copyright notice, this list of
  34. # conditions and the following disclaimer in the documentation and/or other materials provided
  35. # with the distribution.
  36. #
  37. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
  38. # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
  39. # FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
  40. # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  41. # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  42. # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
  43. # IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
  44. # OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  45. import argparse
  46. import sys
  47. import lz4.block
  48. class BinFileArg:
  49. def __init__(self, mode):
  50. self._mode = mode
  51. def __call__(self, arg):
  52. objs = {
  53. "r": sys.stdin.buffer,
  54. "w": sys.stdout.buffer,
  55. }
  56. if arg == "-":
  57. return objs[self._mode]
  58. try:
  59. return open(arg, self._mode + "b")
  60. except OSError as e:
  61. raise argparse.ArgumentTypeError(
  62. "failed to open file for %s: %s" % (
  63. "reading" if self._mode == "r" else "writing",
  64. e
  65. )
  66. )
  67. def decompress(file_obj):
  68. if file_obj.read(8) != b"mozLz40\0":
  69. raise ValueError("Invalid magic number")
  70. return lz4.block.decompress(file_obj.read())
  71. def compress(file_obj):
  72. compressed = lz4.block.compress(file_obj.read())
  73. return b"mozLz40\0" + compressed
  74. def get_argparser():
  75. p = argparse.ArgumentParser(
  76. description="MozLz4a compression/decompression utility"
  77. )
  78. p.add_argument(
  79. "-d", "--decompress", "--uncompress",
  80. action="store_true",
  81. help="Decompress the input file instead of compressing it."
  82. )
  83. p.add_argument(
  84. "in_file",
  85. type=BinFileArg("r"),
  86. help="Path to input file. `-' means standard input."
  87. )
  88. p.add_argument(
  89. "out_file",
  90. type=BinFileArg("w"),
  91. nargs="?",
  92. default="-",
  93. help="Path to output file. `-' means standard output (and is the default)."
  94. )
  95. return p
  96. def main():
  97. args = get_argparser().parse_args()
  98. try:
  99. with args.in_file as fh:
  100. if args.decompress:
  101. data = decompress(fh)
  102. else:
  103. data = compress(fh)
  104. except Exception as e:
  105. print(
  106. "Could not compress/decompress file `%s': %s" % (
  107. args.in_file.name,
  108. e
  109. ),
  110. file=sys.stderr
  111. )
  112. sys.exit(4)
  113. try:
  114. with args.out_file as fh:
  115. fh.write(data)
  116. except Exception as e:
  117. print(
  118. "Could not write to output file `%s': %s" % (
  119. args.out_file.name,
  120. e
  121. ),
  122. file=sys.stderr
  123. )
  124. sys.exit(5)
  125. if __name__ == "__main__":
  126. sys.exit(main())