Skip to content

Commit

Permalink
Fix lz4 and lzma compressed images (#27)
Browse files Browse the repository at this point in the history
* Add simple automated test

* Add missing LZMA support

* Fix broken LZ4 support

Trying to access an LZ4-compressed squashfs file would result in:

    LZ4F_getFrameInfo failed with code: ERROR_frameType_unknown
  • Loading branch information
mxmlnkn authored Apr 21, 2024
1 parent 8f4d3dc commit e637b26
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 3 deletions.
51 changes: 51 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Tests

on:
push:
branches: '**'
tags-ignore: '**'
pull_request:

jobs:
Tests:
runs-on: ${{ matrix.os }}

strategy:
matrix:
os: [ubuntu-latest]
python-version: ['3.7', '3.12']

defaults:
run:
shell: bash

steps:
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- uses: msys2/setup-msys2@v2
if: startsWith( matrix.os, 'windows' )

- name: Install Dependencies (Linux)
if: startsWith( matrix.os, 'ubuntu' )
run: |
sudo apt-get -y install liblzo2-dev
- name: Install pip Dependencies
run: |
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade wheel setuptools twine pytest python-lzo lz4 zstandard
- name: Test Installation From Tarball
run: |
python3 setup.py clean check build sdist bdist_egg bdist_wheel
twine check dist/*
python3 -m pip install "$( find dist -name '*.tar.gz' | head -1 )"
- name: Unit Tests
run: |
pytest PySquashfsImage/tests/test_*.py
37 changes: 34 additions & 3 deletions PySquashfsImage/compressor.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,36 @@ def uncompress(self, src, size, outsize):
return self._lib.decompress(src, False, outsize)


class LZMACompressor(Compressor):
name = "lzma"

def __init__(self):
try:
import lzma
except ImportError:
from backports import lzma
self._lib = lzma

def uncompress(self, src, size, outsize):
# https://github.com/plougher/squashfs-tools/blob/a04910367d64a5220f623944e15be282647d77ba/squashfs-tools/
# lzma_wrapper.c#L40
# res = LzmaCompress(dest + LZMA_HEADER_SIZE, &outlen, src, size, dest,
# &props_size, 5, block_size, 3, 0, 2, 32, 1);
# https://github.com/jljusten/LZMA-SDK/blob/781863cdf592da3e97420f50de5dac056ad352a5/C/LzmaLib.h#L96
# -> level=5, dictSize=block_size, lc=3, lp=0, pb=2, fb=32, numThreads=1
# https://github.com/plougher/squashfs-tools/blob/a04910367d64a5220f623944e15be282647d77ba/squashfs-tools/
# lzma_wrapper.c#L30
# For some reason, squashfs does not store raw lzma but adds a custom header of 5 B and 8 B little-endian
# uncompressed size, which can be read with struct.unpack('<Q', src[5:5+8]))
LZMA_PROPS_SIZE = 5
LZMA_HEADER_SIZE = LZMA_PROPS_SIZE + 8
return self._lib.decompress(
src[LZMA_HEADER_SIZE:],
format=self._lib.FORMAT_RAW,
filters=[{"id": self._lib.FILTER_LZMA1, 'lc': 3, 'lp': 0, 'pb': 2}],
)


class XZCompressor(Compressor):
name = "xz"

Expand All @@ -48,11 +78,11 @@ class LZ4Compressor(Compressor):
name = "lz4"

def __init__(self):
import lz4.frame
self._lib = lz4.frame
import lz4.block
self._lib = lz4.block

def uncompress(self, src, size, outsize):
return self._lib.decompress(src)
return self._lib.decompress(src, outsize)


class ZSTDCompressor(Compressor):
Expand All @@ -69,6 +99,7 @@ def uncompress(self, src, size, outsize):
compressors = {
Compression.NO: Compressor,
Compression.ZLIB: ZlibCompressor,
Compression.LZMA: LZMACompressor,
Compression.LZO: LZOCompressor,
Compression.XZ: XZCompressor,
Compression.LZ4: LZ4Compressor,
Expand Down
38 changes: 38 additions & 0 deletions PySquashfsImage/tests/test_library.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import io
import os
import subprocess
import tarfile
import tempfile

import pytest

import PySquashfsImage


def _createFile(tarArchive, name, contents):
tinfo = tarfile.TarInfo(name)
tinfo.size = len(contents)
tarArchive.addfile(tinfo, io.BytesIO(contents.encode()))


@pytest.mark.parametrize("compression", ["", "gzip", "lz4", "lzma", "lzo", "xz", "zstd"])
def test_compressions(compression):
with tempfile.TemporaryDirectory() as tmpdir:
tarPath = os.path.join(tmpdir, "foo.tar")
with tarfile.open(name=tarPath, mode='w:') as tarArchive:
_createFile(tarArchive, "foo", "bar")

squashfsPath = os.path.join(tmpdir, f"foo.{compression if compression else 'no-compression'}.squashfs")
compressionOptions = ["-comp", compression] if compression else ["-noI", "-noId", "-noD", "-noF", "-noX"]
process = subprocess.Popen(
["sqfstar"] + compressionOptions + [squashfsPath], stdin=subprocess.PIPE, stdout=subprocess.PIPE
)
with open(tarPath, 'rb') as file:
process.communicate(file.read())

with open(squashfsPath, 'rb') as file, PySquashfsImage.SquashFsImage(file) as image:
entries = list(iter(image))
assert len(entries) == 2
assert entries[0].path == "/"
assert entries[1].path == "/foo"
assert image.read_file(entries[1].inode) == b"bar"

0 comments on commit e637b26

Please sign in to comment.