1#!/bin/bash
2#
3#############################################################################
4#
5# 7z2lzma.bash is very primitive .7z to .lzma converter. The input file must
6# have exactly one LZMA compressed stream, which has been created with the
7# default lc, lp, and pb values. The CRC32 in the .7z archive is not checked,
8# and the script may seem to succeed while it actually created a corrupt .lzma
9# file. You should always try uncompressing both the original .7z and the
10# created .lzma and compare that the output is identical.
11#
12# This script requires basic GNU tools and 7z or 7za tool from p7zip.
13#
14# Last modified: 2009-01-15 14:25+0200
15#
16#############################################################################
17#
18# Author: Lasse Collin <lasse.collin@tukaani.org>
19#
20# This file has been put into the public domain.
21# You can do whatever you want with this file.
22#
23#############################################################################
24
25# You can use 7z or 7za, both will work.
26SEVENZIP=7za
27
28if [ $# != 2 -o -z "$1" -o -z "$2" ]; then
29	echo "Usage: $0 input.7z output.lzma"
30	exit 1
31fi
32
33# Converts an integer variable to little endian binary integer.
34int2bin()
35{
36	local LEN=$1
37	local NUM=$2
38	local HEX=(0 1 2 3 4 5 6 7 8 9 A B C D E F)
39	local I
40	for ((I=0; I < "$LEN"; ++I)); do
41		printf "\\x${HEX[(NUM >> 4) & 0x0F]}${HEX[NUM & 0x0F]}"
42		NUM=$((NUM >> 8))
43	done
44}
45
46# Make sure we get possible errors from pipes.
47set -o pipefail
48
49# Get information about the input file. At least older 7z and 7za versions
50# may return with zero exit status even when an error occurred, so check
51# if the output has any lines beginning with "Error".
52INFO=$("$SEVENZIP" l -slt "$1")
53if [ $? != 0 ] || printf '%s\n' "$INFO" | grep -q ^Error; then
54	printf '%s\n' "$INFO"
55	exit 1
56fi
57
58# Check if the input file has more than one compressed block.
59if printf '%s\n' "$INFO" | grep -q '^Block = 1'; then
60	echo "Cannot convert, because the input file has more than"
61	echo "one compressed block."
62	exit 1
63fi
64
65# Get compressed, uncompressed, and dictionary size.
66CSIZE=$(printf '%s\n' "$INFO" | sed -rn 's|^Packed Size = ([0-9]+$)|\1|p')
67USIZE=$(printf '%s\n' "$INFO" | sed -rn 's|^Size = ([0-9]+$)|\1|p')
68DICT=$(printf '%s\n' "$INFO" | sed -rn 's|^Method = LZMA:([0-9]+[bkm]?)$|\1|p')
69
70if [ -z "$CSIZE" -o -z "$USIZE" -o -z "$DICT" ]; then
71	echo "Parsing output of $SEVENZIP failed. Maybe the file uses some"
72	echo "other compression method than plain LZMA."
73	exit 1
74fi
75
76# The following assumes that the default lc, lp, and pb settings were used.
77# Otherwise the output will be corrupt.
78printf '\x5D' > "$2"
79
80# Dictionary size can be either was power of two, bytes, kibibytes, or
81# mebibytes. We need to convert it to bytes.
82case $DICT in
83	*b)
84		DICT=${DICT%b}
85		;;
86	*k)
87		DICT=${DICT%k}
88		DICT=$((DICT << 10))
89		;;
90	*m)
91		DICT=${DICT%m}
92		DICT=$((DICT << 20))
93		;;
94	*)
95		DICT=$((1 << DICT))
96		;;
97esac
98int2bin 4 "$DICT" >> "$2"
99
100# Uncompressed size
101int2bin 8 "$USIZE" >> "$2"
102
103# Copy the actual compressed data. Using multiple dd commands to avoid
104# copying large amount of data with one-byte block size, which would be
105# annoyingly slow.
106BS=8192
107BIGSIZE=$((CSIZE / BS))
108CSIZE=$((CSIZE % BS))
109{
110	dd of=/dev/null bs=32 count=1 \
111		&& dd bs="$BS" count="$BIGSIZE" \
112		&& dd bs=1 count="$CSIZE"
113} < "$1" >> "$2"
114
115exit $?
116