20 Commits

Author SHA1 Message Date
0c5ba4e32c add highlevel description of compressed format in unpack.c 2024-05-25 22:02:47 +02:00
Piotr Fusik
f33bcb2396 mention 6502 unpacker 2024-05-15 10:56:25 +02:00
42Bastian
6d8e13d7e8 add Jaguar RISC versions 2024-04-10 23:53:46 +02:00
88cbda559c add 32bit windows build to releases 2024-03-04 13:07:40 +01:00
f647449497 add --raw-cost and --hexdump flags to help text 2023-08-25 19:42:59 +02:00
7186188118 add --raw-cost option for heatmap reporting 2023-08-25 19:34:17 +02:00
588c1452b9 strip release executables 2023-08-25 00:03:51 +02:00
1e0c8bb5a7 update readme, change --unpack to --decompress, put progress bar on stderr 2023-08-24 22:59:27 +02:00
e11622202b add support to read/write from/to stdin/stdout 2023-08-24 00:00:20 +02:00
795e6c3090 add basic example for compiling upkr to a c library 2023-03-12 13:45:23 +01:00
080db40d00 deny missing docs 2023-01-27 20:26:36 +01:00
5684185+vsariola@users.noreply.github.com
f502bf4e28 optimize dos stub size to save 2 bytes (discovered by qkumba) 2023-01-26 22:04:59 +01:00
2eb8f230ba add documentation, make pbr optional as well 2022-10-26 23:40:41 +02:00
4eab36b9d9 add some api documentation 2022-10-25 23:33:32 +02:00
7cec54f62b make crossterm dependency optional 2022-10-25 22:40:56 +02:00
7fa6be6ff4 implement printing heatmap as hexdump 2022-10-24 23:34:07 +02:00
cab51e06ff implement heatmap calculation 2022-10-23 23:06:09 +02:00
c4fce626da some clean up - move dos unpacker, fix arm32 unpacker formatting 2022-10-19 22:32:57 +02:00
0d7cda06bb Merge pull request #6 from vsariola/dev/x86
implement three versions of a decompression stub for 16-bit x86 DOS
2022-10-19 22:11:04 +02:00
5684185+vsariola@users.noreply.github.com
140678ae20 implement three versions of a decompression stub for 16-bit x86 DOS 2022-10-17 15:50:09 +03:00
26 changed files with 2119 additions and 94 deletions

255
Cargo.lock generated
View File

@@ -10,9 +10,15 @@ checksum = "38d9ff5d688f1c13395289f67db01d4826b46dd694e7580accdc3e8430f2d98e"
[[package]]
name = "autocfg"
version = "1.0.1"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "cc"
@@ -56,6 +62,31 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "crossterm"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
dependencies = [
"bitflags",
"crossterm_winapi",
"libc",
"mio",
"parking_lot",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm_winapi"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c"
dependencies = [
"winapi",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
@@ -70,9 +101,40 @@ checksum = "478ee9e62aaeaf5b140bd4138753d1f109765488581444218d3ddda43234f3e8"
[[package]]
name = "libc"
version = "0.2.108"
version = "0.2.135"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8521a1b57e76b1ec69af7599e75e38e7b7fad6610f037db8c79b127201b5d119"
checksum = "68783febc7782c6c5cb401fbda4de5a9898be1762314da0bb2c10ced61f18b0c"
[[package]]
name = "lock_api"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
"cfg-if",
]
[[package]]
name = "mio"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf"
dependencies = [
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.36.1",
]
[[package]]
name = "num-traits"
@@ -83,6 +145,29 @@ dependencies = [
"autocfg",
]
[[package]]
name = "parking_lot"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-sys 0.42.0",
]
[[package]]
name = "pbr"
version = "1.0.4"
@@ -113,6 +198,15 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [
"bitflags",
]
[[package]]
name = "sacabase"
version = "2.0.0"
@@ -122,6 +216,48 @@ dependencies = [
"num-traits",
]
[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "signal-hook"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-mio"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
dependencies = [
"libc",
"mio",
"signal-hook",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
dependencies = [
"libc",
]
[[package]]
name = "smallvec"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[package]]
name = "syn"
version = "1.0.101"
@@ -160,7 +296,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
dependencies = [
"libc",
"wasi",
"wasi 0.10.0+wasi-snapshot-preview1",
"winapi",
]
@@ -172,10 +308,11 @@ checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"
[[package]]
name = "upkr"
version = "0.2.0"
version = "0.2.2"
dependencies = [
"anyhow",
"cdivsufsort",
"crossterm",
"lexopt",
"pbr",
"thiserror",
@@ -187,6 +324,12 @@ version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "winapi"
version = "0.3.9"
@@ -208,3 +351,103 @@ name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
dependencies = [
"windows_aarch64_msvc 0.36.1",
"windows_i686_gnu 0.36.1",
"windows_i686_msvc 0.36.1",
"windows_x86_64_gnu 0.36.1",
"windows_x86_64_msvc 0.36.1",
]
[[package]]
name = "windows-sys"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc 0.42.0",
"windows_i686_gnu 0.42.0",
"windows_i686_msvc 0.42.0",
"windows_x86_64_gnu 0.42.0",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc 0.42.0",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
[[package]]
name = "windows_aarch64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
[[package]]
name = "windows_i686_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
[[package]]
name = "windows_i686_gnu"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
[[package]]
name = "windows_i686_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
[[package]]
name = "windows_i686_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
[[package]]
name = "windows_x86_64_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
[[package]]
name = "windows_x86_64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"

View File

@@ -1,14 +1,18 @@
[package]
name = "upkr"
version = "0.2.0"
version = "0.2.2"
edition = "2021"
[profile.release]
strip = "debuginfo"
[features]
terminal = ["crossterm", "pbr"]
[dependencies]
cdivsufsort = "2"
lexopt = "0.2.1"
anyhow = "1"
thiserror = "1.0.36"
pbr = "1"
pbr = { version = "1", optional = true }
crossterm = { version = "0.25.0", default-features = false, optional = true }

View File

@@ -20,20 +20,36 @@ is both about twice as fast and smaller than the Shrinkler unpacker.
The release includes a reference c unpacker, as well as some optimized asm unpackers (arm and riscv). The unpckers in
c_unpacker and asm_unpackers unpack the default upkr compressed format. The z80_unpacker
is based on some variations to the compressed format. (Use `upkr --z80` to select those variations.)
An optimized x86 (DOS) unpacker is currently being worked on out of tree.
The 16 bit dos unpacker also uses some variations. (`upkr --x86`)
### More unpackers outside this repository
* [Atari Lynx](https://github.com/42Bastian/new_bll/blob/master/demos/depacker/unupkr.asm)
* [Atari Jaguar](https://github.com/42Bastian/new_bjl/blob/main/exp/depacker/unupkr.js)
* [8080, R800](https://github.com/ivagorRetrocomp/DeUpkr)
* [6502](https://github.com/pfusik/upkr6502)
## Usage
```
upkr [-l level(0-9)] [config options] <infile> [<outfile>]
upkr -u [config options] <infile> [<outfile>]
upkr --heatmap [config options] <infile> [<outfile>]
upkr --margin [config options] <infile>
-l, --level N compression level 0-9
-0, ..., -9 short form for setting compression level
-u, --unpack unpack infile
-d, --decompress decompress infile
--heatmap calculate heatmap from compressed file
--raw-cost report raw cost of literals in heatmap
(the cost of literals is spread across all matches
that reference the literal by default.)
--hexdump print heatmap as colored hexdump
--margin calculate margin for overlapped unpacking of a packed file
When no infile is given, or the infile is '-', read from stdin.
When no outfile is given and reading from stdin, or when outfile is '-', write to stdout.
Config presets for specific unpackers:
--z80 --big-endian-bitstream --invert-bit-encoding --simplified-prob-update -9
--x86 --bitstream --invert-is-match-bit --invert-continue-value-bit --invert-new-offset-bit
@@ -56,3 +72,13 @@ Config options to tailor output to specific optimized unpackers:
--max-offset N
--max-length N
```
## Heatmap
By default, the `--heatmap` flag writes out the heatmap data as a binary file. The heatmap file is
the same size as the unpacked data. Each byte can be interpreted like this:
```
is_literal = byte & 1; // whether the byte was encoded as a literal (as opposed to a match)
size_in_bits = 2.0 ** (((byte >> 1) - 64) / 8.0); // the size this byte takes up in the compressed data
```

View File

@@ -17,7 +17,7 @@
upkr_unpack:
push { r3-r11, lr }
mov r2, #384
mov r2, #384
mov r3, #128
.Lclear:
subs r2, r2, #1
@@ -29,7 +29,7 @@ upkr_unpack:
bl upkr_decode_bit
bcc .Ldata
.Lmatch:
mov r5, #256
mov r5, #256
rsbs r6, r4, #0
blcc upkr_decode_bit
bcc .Lskip_offset
@@ -39,7 +39,7 @@ upkr_unpack:
popeq { r3-r11, pc }
.Lskip_offset:
mov r5, #256+64
mov r5, #256+64
bl upkr_decode_length
.Lcopy_loop:
ldrb r5, [r0, r3]
@@ -55,46 +55,46 @@ upkr_unpack:
.Ldata_loop:
bl upkr_decode_bit
adc r5, r5, r5
movs r4, r5, lsr #8
movs r4, r5, lsr #8
beq .Ldata_loop
b .Lstore
b .Lstore
.type upkr_decode_length, %function
upkr_decode_length:
mov r12, lr
mov r12, lr
mov r4, #0
mvn r6, #0
mvn r6, #0
.Lbit_loop:
bl upkr_decode_bit_inc
addcc r4, r4, r6
movcc pc, r12
addcc r4, r4, r6
movcc pc, r12
bl upkr_decode_bit_inc
addcs r4, r4, r6
mov r6, r6, lsl #1
mov r6, r6, lsl #1
b .Lbit_loop
.type upkr_decode_bit, %function
upkr_decode_bit_inc:
add r5, r5, #1
add r5, r5, #1
upkr_decode_bit:
cmp r2, #4096
ldrltb r8, [r1], #1
orrlt r2, r8, r2, lsl#8
blt upkr_decode_bit
cmp r2, #4096
ldrltb r8, [r1], #1
orrlt r2, r8, r2, lsl#8
blt upkr_decode_bit
ldrb r8, [sp, -r5]
and r9, r2, #255
add r9, r9, #1
cmp r8, r9
rsbcs r8, r8, #256
mvn r9, r2, lsr#8
addcs r9, r9, #1
mla r2, r8, r9, r2
add r9, r8, #8
sub r8, r8, r9, lsr#4
rsbcs r8, r8, #256
strb r8, [sp, -r5]
mov pc, r14
and r9, r2, #255
add r9, r9, #1
cmp r8, r9
rsbcs r8, r8, #256
mvn r9, r2, lsr#8
addcs r9, r9, #1
mla r2, r8, r9, r2
add r9, r8, #8
sub r8, r8, r9, lsr#4
rsbcs r8, r8, #256
strb r8, [sp, -r5]
mov pc, r14

View File

@@ -0,0 +1,190 @@
;;; -*-asm-*-
;;; ukpr unpacker for Atari Jaguar RISC.
;;; lyxass syntax
; input:
;;; R20 : packed buffer
;;; R21 : output buffer
;;; r30 : return address
;;;
;;; Register usage (destroyed!)
;;; r0-r17,r20,r21
;;;
DST REG 21
SRC REG 20
REGTOP 16
LR_save REG 99
LR_save2 REG 99
GETBIT REG 99
GETLENGTH REG 99
LITERAL REG 99
LOOP REG 99
index REG 99
bit_pos REG 99
state REG 99
prev_was_match REG 99
offset REG 99
prob reg 99
byte REG 99
PROBS reg 99
tmp2 reg 2
tmp1 REG 1
tmp0 REG 0
REGMAP
upkr_probs equ $200
SIZEOF_PROBS EQU 1+255+1+2*32+2*32
unupkr::
move LR,LR_save
moveq #0,tmp0
movei #upkr_probs,PROBS
bset #7,tmp0
movei #SIZEOF_PROBS,tmp2
move PROBS,tmp1
.init storeb tmp0,(tmp1)
subq #1,tmp2
jr pl,.init
addq #1,tmp1
moveq #0,offset
moveq #0,state
movei #getlength,GETLENGTH
movei #getbit,GETBIT
.looppc move PC,LOOP
addq #.loop-.looppc,LOOP
move pc,LITERAL
jr .start
addq #6,LITERAL
.literal
moveq #1,byte
move pc,LR
jr .into
addq #6,LR ; LR = .getbit
.getbit
addc byte,byte
.into
btst #8,byte
jump eq,(GETBIT)
move byte,index
storeb byte,(DST)
addq #1,DST
.start
moveq #0,prev_was_match
.loop
moveq #0,index
BL (GETBIT)
jump cc,(LITERAL)
addq #14,LR
cmpq #1,prev_was_match
jr eq,.newoff
shlq #8,r0
jump (GETBIT)
move r0,index
jr cc,.oldoff
shlq #8,r0
.newoff
addq #1,r0 ; r0 = 257
BL (GETLENGTH)
subq #1,r0
jump eq,(LR_save)
move r0,offset
.oldoff
movei #257+64,r0
BL (GETLENGTH)
move DST,r1
sub offset,r1
.cpymatch1
loadb (r1),r2
subq #1,r0
addqt #1,r1
storeb r2,(DST)
jr ne,.cpymatch1
addq #1,DST
jump (LOOP)
moveq #1,prev_was_match
getlength:
move LR,LR_save2
moveq #0,byte
move r0,index
moveq #0,bit_pos
move pc,LR
jump (GETBIT)
addq #6,LR
.gl
jr cc,.exit
addq #8,LR ; => return to "sh ..."
jump (GETBIT)
nop
sh bit_pos,r0
subq #1,bit_pos ; sh < 0 => shift left!
or r0,byte
jump (GETBIT)
subq #8,LR
.exit
moveq #1,r0
sh bit_pos,r0
jump (LR_save2)
or byte,r0
.newbyte:
loadb (SRC),r2
shlq #8,state
addq #1,SRC
or r2,state
getbit
move state,r2
move PROBS,r1
add index,r1 ; r1 = &probs[index]
shrq #12,r2
loadb (r1),prob
jr eq,.newbyte
move state,r2
move state,r0
shlq #24,r2
shrq #8,r0 ; sh
shrq #24,r2 ; sl
cmp prob,r2
addqt #1,index
jr cs,.one
mult prob,r0
;; state -= ((state >> 8) + 1)*prob
;; prob -= (prob+8)>>4
move prob,r2
add prob,r0
addq #8,r2
sub r0,state
shrq #4,r2
moveq #0,r0
jr .ret
sub r2,prob
.one
;; state = (state >> 8)*prob+(state & 0xff)
;; prob += (256 + 8 - prob) >> 4
move r2,state
movei #256+8,r2
add r0,state
sub prob,r2 ; 256-prob+8
shrq #4,r2
add r2,prob
moveq #3,r0
.ret
storeb prob,(r1)
jump (LR)
shrq #1,r0 ; C = 0, r0 = 1

View File

@@ -0,0 +1,217 @@
;;; -*-asm-*-
;;; ukpr unpacker for Atari Jaguar RISC. (quick version)
;;; lyxass syntax
; input:
;;; R20 : packed buffer
;;; R21 : output buffer
;;; r30 : return address
;;;
;;; Register usage (destroyed!)
;;; r0-r17,r20,r21
;;;
DST REG 21
SRC REG 20
REGTOP 17
LR_save REG 99
LR_save2 REG 99
GETBIT REG 99
GETLENGTH REG 99
LITERAL REG 99
LOOP REG 99
index REG 99
bit_pos REG 99
state REG 99
prev_was_match REG 99
offset REG 99
prob reg 99
byte REG 99
ndata reg 99
PROBS reg 99
tmp2 reg 2
tmp1 REG 1
tmp0 REG 0
REGMAP
upkr_probs equ $200
SIZEOF_PROBS EQU 1+255+1+2*32+2*32
unupkr::
move LR,LR_save
movei #$80808080,tmp0
movei #upkr_probs,PROBS
movei #SIZEOF_PROBS,tmp2
move PROBS,tmp1
.init store tmp0,(tmp1)
subq #4,tmp2
jr pl,.init
addq #4,tmp1
loadb (SRC),ndata
addq #1,SRC
moveq #0,offset
moveq #0,state
movei #getlength,GETLENGTH
movei #getbit,GETBIT
.looppc move PC,LOOP
addq #.loop-.looppc,LOOP
move pc,LITERAL
jr .start
addq #6,LITERAL
.literal
moveq #1,byte
move pc,LR
jr .into
addq #6,LR ; LR = .getbit
.getbit
addc byte,byte
.into
btst #8,byte
jump eq,(GETBIT)
move byte,index
storeb byte,(DST)
addq #1,DST
.start
moveq #0,prev_was_match
.loop
moveq #0,index
BL (GETBIT)
jump cc,(LITERAL)
addq #14,LR
cmpq #1,prev_was_match
jr eq,.newoff
shlq #8,r0
jump (GETBIT)
move r0,index
jr cc,.oldoff
shlq #8,r0
.newoff
addq #1,r0 ; r0 = 257
BL (GETLENGTH)
subq #1,r0
move r0,offset
jump eq,(LR_save)
nop
.oldoff
movei #257+64,r0
BL (GETLENGTH)
move DST,r2
move DST,r1
or offset,r2
btst #0,r2
moveq #1,prev_was_match
jr ne,.cpymatch1
sub offset,r1
.cpymatch2
loadw (r1),r2
addqt #2,r1
subq #2,r0
storew r2,(DST)
jump eq,(LOOP)
addqt #2,DST
jr pl,.cpymatch2
nop
jump (LOOP)
subq #1,DST
.cpymatch1
loadb (r1),r2
subq #1,r0
addqt #1,r1
storeb r2,(DST)
jr ne,.cpymatch1
addq #1,DST
jump (LOOP)
//-> nop
getlength:
move LR,LR_save2
moveq #0,byte
move r0,index
moveq #0,bit_pos
move pc,LR
jump (GETBIT)
addq #6,LR
.gl
jr cc,.exit
addq #8,LR ; => return to "sh ..."
jump (GETBIT)
nop
sh bit_pos,r0
subq #1,bit_pos ; sh < 0 => shift left!
or r0,byte
jump (GETBIT)
subq #8,LR
.exit
moveq #1,r0
sh bit_pos,r0
jump (LR_save2)
or byte,r0
.newbyte:
move ndata,r2
shlq #8,state
loadb (SRC),ndata
or r2,state
addq #1,SRC
move state,r2
shrq #12,r2
jr ne,.done
move state,r2
jr .newbyte
getbit
move state,r2
move PROBS,r1
add index,r1 ; r1 = &probs[index]
shrq #12,r2
loadb (r1),prob
jr eq,.newbyte
move state,r2
.done
move state,r0
shlq #24,r2
shrq #8,r0 ; sh
shrq #24,r2 ; sl
cmp prob,r2
addqt #1,index
jr cs,.one
mult prob,r0
;; state -= ((state >> 8) + 1)*prob
;; prob -= (prob+8)>>4
move prob,r2
add prob,r0
addq #8,r2
sub r0,state
shrq #4,r2
moveq #0,r0
sub r2,prob
shrq #1,r0 ; C = 0, r0 = 0
jump (LR)
storeb prob,(r1)
.one
;; state = (state >> 8)*prob+(state & 0xff)
;; prob += (256 + 8 - prob) >> 4
move r2,state
movei #256+8,r2
add r0,state
sub prob,r2 ; 256-prob+8
shrq #4,r2
add r2,prob
moveq #3,r0
storeb prob,(r1)
jump (LR)
shrq #1,r0 ; C = 0, r0 = 1

2
c_library/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/target/
/upkr

127
c_library/Cargo.lock generated Normal file
View File

@@ -0,0 +1,127 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "anyhow"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800"
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "cc"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
[[package]]
name = "cdivsufsort"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edefce019197609da416762da75bb000bbd2224b2d89a7e722c2296cbff79b8c"
dependencies = [
"cc",
"sacabase",
]
[[package]]
name = "lexopt"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478ee9e62aaeaf5b140bd4138753d1f109765488581444218d3ddda43234f3e8"
[[package]]
name = "num-traits"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
"autocfg",
]
[[package]]
name = "proc-macro2"
version = "1.0.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
dependencies = [
"proc-macro2",
]
[[package]]
name = "sacabase"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9883fc3d6ce3d78bb54d908602f8bc1f7b5f983afe601dabe083009d86267a84"
dependencies = [
"num-traits",
]
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5ab016db510546d856297882807df8da66a16fb8c4101cb8b30054b0d5b2d9c"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
[[package]]
name = "upkr"
version = "0.2.1"
dependencies = [
"anyhow",
"cdivsufsort",
"lexopt",
"thiserror",
]
[[package]]
name = "upkr_c"
version = "0.0.1"
dependencies = [
"upkr",
]

17
c_library/Cargo.toml Normal file
View File

@@ -0,0 +1,17 @@
[package]
name = "upkr_c"
version = "0.0.1"
edition = "2021"
[lib]
name = "upkr"
crate-type = ["staticlib"]
[profile.release]
opt-level = "s"
strip = "debuginfo"
lto = true
panic = "abort"
[dependencies]
upkr = { path="..", default-features=false }

8
c_library/Makefile Normal file
View File

@@ -0,0 +1,8 @@
upkr: upkr.c upkr.h target/release/libupkr.a
gcc -O2 -Ltarget/release -o upkr upkr.c -lupkr -lm
strip upkr
target/release/libupkr.a: cargo
cargo build --release
.PHONY: cargo

11
c_library/Readme.md Normal file
View File

@@ -0,0 +1,11 @@
This is a simple example of compiling upkr to a library that can be linked in a
c program. It consists of a small rust crate which implements the c api and
compiles to a static library and a matching c header file. As is, the rust
crate offers two simple functions to compress/uncompress data with the default
upkr config.
The provided makefile will only work on linux. Building the example upkr.c on
other platforms is left as an exercise for the reader ;)
On Windows you might have to make sure to install and use the correct rust
toolchain version (mingw vs. msvc) to match your c compiler.

42
c_library/src/lib.rs Normal file
View File

@@ -0,0 +1,42 @@
use std::ffi::c_int;
// the upkr config to use, this can be modified to use other configs
fn config() -> upkr::Config {
upkr::Config::default()
}
#[no_mangle]
pub extern "C" fn upkr_compress(
output_buffer: *mut u8,
output_buffer_size: usize,
input_buffer: *const u8,
input_size: usize,
compression_level: c_int,
) -> usize {
let output_buffer = unsafe { std::slice::from_raw_parts_mut(output_buffer, output_buffer_size) };
let input_buffer = unsafe { std::slice::from_raw_parts(input_buffer, input_size) };
let packed_data = upkr::pack(input_buffer, compression_level.max(0).min(9) as u8, &config(), None);
let copy_size = packed_data.len().min(output_buffer.len());
output_buffer[..copy_size].copy_from_slice(&packed_data[..copy_size]);
packed_data.len()
}
#[no_mangle]
pub extern "C" fn upkr_uncompress(output_buffer: *mut u8, output_buffer_size: usize, input_buffer: *const u8, input_size: usize) -> isize {
let output_buffer = unsafe { std::slice::from_raw_parts_mut(output_buffer, output_buffer_size)};
let input_buffer = unsafe { std::slice::from_raw_parts(input_buffer, input_size)};
match upkr::unpack(input_buffer, &config(), output_buffer.len()) {
Ok(unpacked_data) => {
output_buffer[..unpacked_data.len()].copy_from_slice(&unpacked_data);
unpacked_data.len() as isize
}
Err(upkr::UnpackError::OverSize { size, .. }) => size as isize,
Err(other) => {
eprintln!("[upkr] compressed data corrupt: {}", other);
-1
}
}
}

99
c_library/upkr.c Normal file
View File

@@ -0,0 +1,99 @@
#include "upkr.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char** argv) {
if(argc < 2) {
fprintf(stdout, "Usage:\n upkr [compress] [-0 .. -9] <file> [<out-file>]\n upkr [uncompress] <file> [<out-file>]\n");
return 1;
}
int argi = 1;
int uncompress = 0;
int compression_level = 4;
if(strcmp(argv[argi], "compress") == 0) {
++argi;
} else if(strcmp(argv[argi], "uncompress") == 0) {
uncompress = 1;
++argi;
}
if(argi < argc && argv[argi][0] == '-') {
compression_level = atoi(argv[argi] + 1);
++argi;
}
if(argi == argc) {
fprintf(stdout, "intput filename missing\n");
return 1;
}
const char* input_name = argv[argi++];
char* output_name;
if(argi < argc) {
output_name = argv[argi];
} else {
output_name = malloc(strlen(input_name) + 5);
strcpy(output_name, input_name);
strcat(output_name, uncompress ? ".unp" : ".upk");
}
FILE* file = fopen(input_name, "rb");
if(file == 0) {
fprintf(stdout, "failed to open input file '%s'\n", file);
return 1;
}
fseek(file, 0, SEEK_END);
long input_size = ftell(file);
rewind(file);
char* input_buffer = (char*)malloc(input_size);
long offset = 0;
while(offset < input_size) {
long read_size = fread(input_buffer + offset, 1, input_size - offset, file);
if(read_size <= 0) {
fprintf(stdout, "error reading input file\n");
return 1;
}
offset += read_size;
}
fclose(file);
long output_buffer_size = input_size * 8;
long output_size;
char* output_buffer = (char*)malloc(output_buffer_size);
for(;;) {
if(uncompress) {
output_size = upkr_uncompress(output_buffer, output_buffer_size, input_buffer, input_size);
} else {
output_size = upkr_compress(output_buffer, output_buffer_size, input_buffer, input_size, compression_level);
}
if(output_size < 0) {
return 1;
}
if(output_size <= output_buffer_size) {
break;
}
output_buffer = (char*)realloc(output_buffer, output_size);
output_buffer_size = output_size;
}
file = fopen(output_name, "wb");
if(file == 0) {
fprintf(stdout, "failed to open output file '%s'\n", output_name);
return 1;
}
offset = 0;
while(offset < output_size) {
long written_size = fwrite(output_buffer + offset, 1, output_size - offset, file);
if(written_size <= 0) {
fprintf(stdout, "error writing output file\n");
return 1;
}
offset += written_size;
}
fclose(file);
return 0;
}

25
c_library/upkr.h Normal file
View File

@@ -0,0 +1,25 @@
#ifndef UPKR_H_INCLUDED
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
// input_buffer/input_size: input data to compress
// output_buffer/output_buffer_size: buffer to compress into
// compression_level: 0-9
// returns the size of the compressed data, even if it didn't fit into the output buffer
size_t upkr_compress(void* output_buffer, size_t output_buffer_size, void* input_buffer, size_t input_size, int compression_level);
// input_buffer/input_size: compressed data
// output_buffer/output_buffer_size: buffer to uncompress into
// return value:
// >= 0 : size of uncompressed data, even if it didn't fit into the output buffer
// < 0 : input data corrupt, unable to decompress
ptrdiff_t upkr_uncompress(void* output_buffer, size_t output_buffer_size, void* input_buffer, size_t input_size);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -1,3 +1,56 @@
/*
A simple C unpacker for upkr compressed data.
This implements two variants, selected by the UPKR_BITSTREAM define:
- normal: faster and smaller on modern hardware as whole bytes are shifted into
the rANS state at a time, but requires 20bits for the state
- bitstream: only single bits are shifted into the rANS state at a time
which allows the state to always fit in 16bits which is a boon
on very old CPUs.
The encoder and decoder need to be configured to use the same varianet.
upkr compressed data is a rANS byte-/bit-stream encoding a series of literal
byte values and back-references as probability encoded bits.
upkr_decode_bit reads one bit from the rANS stream, taking a probability context
as parameter. The probability context is a byte estimating the probability of
a bit encoded in this context being set. It is updated by upkr_decode_bit
after each decoded bit to reflect the observed past frequencies of on/off bits.
There are a number of different contexts used in the compressed format. The order in the
upkr_probs array is arbitrary, the only requirement for the unpacker is that all bits
that shared the same context while encoding also share the same context while decoding.
The contexts are:
- is match
- has offset
- literal bit N (0-7) with already decoded highest bits of literal == M (255 total)
- offset bit N (one less than max offset bits)
- has offset bit N (max offset bits)
- length bit N (one less then max length bits)
- has length bit N (max length bits)
Literal bytes are encoded from highest to lowest bit, with the bit position and
the already decoded bits as context.
Offst and Length are encoded in an interlaced variant of elias gamma coding. They
are encoded from lowest to highest bits. For each bit, first one bit is read in the
"has offset/length bit N)". If this is set, offset/length bit N is read in it's context
and the decoding continues with the next bit. If the "has bit N" is read as false, a
fixed 1 bit is added as the top bit at this position.
The highlevel decode loop then looks like this:
loop:
if read_bit(IS_MATCH):
if prev_was_match || read_bit(HAS_OFFSET):
offset = read_length_or_offset(OFFSET) - 1
if offset == 0:
break
length = read_length_or_offset(LENGTH)
copy_bytes_from_offset(length, offset)
else:
read_and_push(literal)
*/
typedef unsigned char u8;
typedef unsigned short u16;
typedef unsigned long u32;
@@ -14,6 +67,7 @@ u32 upkr_state;
int upkr_decode_bit(int context_index) {
#ifdef UPKR_BITSTREAM
// shift in single bits until rANS state is >= 32768
while(upkr_state < 32768) {
if(upkr_bits_left == 0) {
upkr_current_byte = *upkr_data_ptr++;
@@ -24,6 +78,7 @@ int upkr_decode_bit(int context_index) {
--upkr_bits_left;
}
#else
// shift in a full byte until rANS state is >= 4096
while(upkr_state < 4096) {
upkr_state = (upkr_state << 8) | *upkr_data_ptr++;
}
@@ -32,6 +87,8 @@ int upkr_decode_bit(int context_index) {
int prob = upkr_probs[context_index];
int bit = (upkr_state & 255) < prob ? 1 : 0;
// rANS state and context probability update
// for the later, add 1/16th (rounded) of difference from either 0 or 256
if(bit) {
upkr_state = prob * (upkr_state >> 8) + (upkr_state & 255);
prob += (256 - prob + 8) >> 4;
@@ -60,6 +117,7 @@ void* upkr_unpack(void* destination, void* compressed_data) {
#ifdef UPKR_BITSTREAM
upkr_bits_left = 0;
#endif
// all contexts are initialized to 128 = equal probability of 0 and 1
for(int i = 0; i < sizeof(upkr_probs); ++i)
upkr_probs[i] = 128;
@@ -68,10 +126,13 @@ void* upkr_unpack(void* destination, void* compressed_data) {
int prev_was_match = 0;
int offset = 0;
for(;;) {
// is match
if(upkr_decode_bit(0)) {
// has offset
if(prev_was_match || upkr_decode_bit(256)) {
offset = upkr_decode_length(257) - 1;
if(offset == 0) {
// a 0 offset signals the end of the compressed data
break;
}
}
@@ -82,6 +143,9 @@ void* upkr_unpack(void* destination, void* compressed_data) {
}
prev_was_match = 1;
} else {
// byte contains the previously read bits and indicates the number of
// read bits by the set top bit. Therefore it can be directly used as the
// context index. The set top bit ends up at bit position 8 and is not stored.
int byte = 1;
while(byte < 256) {
int bit = upkr_decode_bit(byte);

13
dos_unpacker/readme.txt Normal file
View File

@@ -0,0 +1,13 @@
16 bit DOS executable stubs
---------------------------
by pestis and TomCat
unpack_x86_16_DOS.asm:
maximum compatibility, relocates unpacked code to normal start address
unpack_x86_16_DOS_no_relocation.asm:
saves some bytes by not relocating, unpacked code needs to be assembled to
start at 0x3FFE
unpack_x86_16_DOS_no_repeated_offset.asm:
removes support for repeated offsets, potentially at the cost of some compression ratio.
most likely only a win in very narrow circumstances around the 1kb mark

View File

@@ -0,0 +1,160 @@
; Contributions from pestis, TomCat and exoticorn
;
; This is the 16-bit DOS x86 decompression stub for upkr, which is designed for
; maximum compatibility: it relocates the compressed data so it can be
; decompressed starting at the normal .COM starting address. In other words,
; many of the already existing .COM files should be compressable using this
; stub.
;
; How to use:
; 1) Pack your intro using upkr into data.bin with the --x86 command line
; argument:
;
; $ upkr --x86 intro.com data.bin
;
; 2) Compile this .asm file using nasm (or any compatible assembler):
;
; $ nasm unpack_x86_16_DOS.asm -fbin -o intropck.com
;
; The packed size of the intro+stub is limited by max_len (see below) bytes.
;
; In specific cases, the unpacker stub can be further optimized to save a byte
; or two:
; 1) You can remove CLC before RET, if you don't mind carry being set upon
; program entry
; 2) You can also move PUSHA before PUSH SI and put POPA as the first
; operation of the compressed code.
max_len equ 16384
prog_start equ (0x100+max_len+510+relocation-upkr_unpack)
probs equ (((prog_start+max_len+510)+255)/256)*256
org 0x100
; This is will be loaded at 0x100, but relocates the code and data to prog_start
relocation:
push si ; si = 0x100 at DOS start, so save it for later ret
pusha ; pusha to recall all registers before starting intro
push si ; for pop di to start writing the output
mov di, prog_start ; the depacker & data are relocated from 0x100 to prog_start
mov ch, max_len/512
rep movsw
jmp si ; jump to relocated upkr_unpack
; upkr_unpack unpacks the code to 0x100 and runs it when done.
upkr_unpack:
xchg ax, bp ; position in input bitstream (bp) = 0
cwd ; upkr_state (dx) = 0;
xchg ax, cx ; cx = 0x9XX
mov al, 128 ; for(int i = 0; i < sizeof(upkr_probs); ++i) upkr_probs[i] = 128;
rep stosb
pop di ; u8* write_ptr = (u8*)destination;
.mainloop:
mov bx, probs
call upkr_decode_bit
jc .else ; if(upkr_decode_bit(0)) {
mov bh, (probs+256)/256
jcxz .skip_call
call upkr_decode_bit
jc .skipoffset
.skip_call:
stc
call upkr_decode_number ; offset = upkr_decode_length(258) - 1;
loop .notdone ; if(offset == 0)
popa
clc
ret
.notdone:
mov si, di
.sub:
dec si
loop .sub
.skipoffset:
mov bl, 128 ; int length = upkr_decode_length(384);
call upkr_decode_number
rep movsb ; *write_ptr = write_ptr[-offset];
jmp .mainloop
.byteloop:
call upkr_decode_bit ; int bit = upkr_decode_bit(byte);
.else:
adc bl, bl ; byte = (byte << 1) + bit;
jnc .byteloop
xchg ax, bx
stosb
inc si
mov cl, 1
jmp .mainloop ; prev_was_match = 0;
; upkr_decode_bit decodes one bit from the rANS entropy encoded bit stream.
; parameters:
; bx = memory address of the context probability
; dx = decoder state
; bp = bit position in input stream
; returns:
; dx = new decoder state
; bp = new bit position in input stream
; carry = bit
; trashes ax
upkr_load_bit:
bt [compressed_data-relocation+prog_start], bp
inc bp
adc dx, dx
upkr_decode_bit:
inc dx ; inc dx, dec dx is used to test the top (sign) bit of dx
dec dx
jns upkr_load_bit
movzx ax, byte [bx] ; u16 prob = upkr_probs[context_index]
neg byte [bx]
push ax ; save prob, tmp = prob
cmp dl, al ; int bit = (upkr_state & 255) < prob ? 1 : 0; (carry = bit)
pushf ; save bit flags
jc .bit ; (skip if bit)
xchg [bx], al ; tmp = 256 - tmp;
.bit:
shr byte [bx], 4 ; upkr_probs[context_index] = tmp + (256 - tmp + 8) >> 4;
adc [bx], al
mul dh ; upkr_state = tmp * (upkr_state >> 8) + (upkr_state & 255);
mov dh, 0
add dx, ax
popf
pop ax
jc .bit2 ; (skip if bit)
neg byte [bx] ; tmp = 256 - tmp;
sub dx, ax ; upkr_state -= prob; note that this will also leave carry always unset, which is what we want
.bit2:
ret ; return the bit in carry
; upkr_decode_number loads a variable length encoded number (up to 16 bits) from
; the compressed stream. Only numbers 1..65535 can be encoded. If the encoded
; number has 4 bits and is 1ABC, it is encoded using a kind of an "interleaved
; elias code": 0A0B0C1. The 1 in the end implies that no more bits are coming.
; parameters:
; cx = must be 0
; bx = memory address of the context probability
; dx = decoder state
; bp = bit position in input stream
; carry = must be 1
; returns:
; cx = length
; dx = new decoder state
; bp = new bit position in input stream
; carry = 1
; trashes bl, ax
upkr_decode_number_loop:
inc bx
call upkr_decode_bit
upkr_decode_number:
rcr cx, 1
inc bx
call upkr_decode_bit
jnc upkr_decode_number_loop ; 0 = there's more bits coming, 1 = no more bits
.loop2:
rcr cx, 1
jnc .loop2
ret
compressed_data:
incbin "data.bin"

View File

@@ -0,0 +1,151 @@
; Contributions from pestis, TomCat and exoticorn
;
; This is the 16-bit DOS x86 decompression stub for upkr, which decompresses the
; code starting at address 0x3FFE (or whatever is defined by the entrypoint
; below). Thus, the packed code needs to be assembled with org 0x3FFE to work.
;
; How to use:
; 1) Put POPA as the first instruction of your compiled code and use org
; 0x3FFE
; 2) Pack your intro using upkr into data.bin with the --x86 command line
; argument:
;
; $ upkr --x86 intro.com data.bin
;
; 2) Compile this .asm file using nasm (or any compatible assembler) e.g.
;
; $ nasm unpack_x86_16_DOS_no_relocation.asm -fbin -o intropck.com
;
; In specific cases, the unpacker stub can be further optimized to save a byte
; or two:
; 1) If your stub+compressed code is 2k or smaller, you can save 1 byte by
; putting probs at 0x900 and initializing DI with SALC; XCHG AX, DI instead
; of MOV DI, probs
; 2) If you remove the PUSHA (and POPA in the compressed code), then you can
; assume the registers as follows: AX = 0x00XX, BX = probs + 0x1XX, CX = 0
; DX = (trash), SI = DI = right after your program, SP = as it was when the
; program started, flags = carry set
;
; Note that even with the PUSHA / POPA, carry will be set (!) unlike normal dos
; program.
entry equ 0x3FFE
probs equ entry - 0x1FE ; must be aligned to 256
org 0x100
; This is will be loaded at 0x100, but relocates the code and data to prog_start
upkr_unpack:
pusha
xchg ax, bp ; position in bitstream = 0
cwd ; upkr_state = 0;
mov di, probs
mov ax, 0x8080 ; for(int i = 0; i < sizeof(upkr_probs); ++i) upkr_probs[i] = 128;
rep stosw
push di
.mainloop:
mov bx, probs
call upkr_decode_bit
jc .else ; if(upkr_decode_bit(0)) {
mov bh, (probs+256)/256
jcxz .skip_call ; if(prev_was_match || upkr_decode_bit(257)) {
call upkr_decode_bit
jc .skipoffset
.skip_call:
stc
call upkr_decode_number ; offset = upkr_decode_number(258) - 1;
mov si, di
loop .sub ; if(offset == 0)
ret
.sub:
dec si
loop .sub
.skipoffset:
mov bl, 128 ; int length = upkr_decode_number(384);
call upkr_decode_number
rep movsb ; *write_ptr = write_ptr[-offset];
jmp .mainloop
.byteloop:
call upkr_decode_bit ; int bit = upkr_decode_bit(byte);
.else:
adc bl, bl ; byte = (byte << 1) + bit;
jnc .byteloop
xchg ax, bx
stosb
inc si
mov cl, 1
jmp .mainloop ; prev_was_match = 0;
; upkr_decode_bit decodes one bit from the rANS entropy encoded bit stream.
; parameters:
; bx = memory address of the context probability
; dx = decoder state
; bp = bit position in input stream
; returns:
; dx = new decoder state
; bp = new bit position in input stream
; carry = bit
; trashes ax
upkr_load_bit:
bt [compressed_data], bp
inc bp
adc dx, dx
upkr_decode_bit:
inc dx
dec dx ; inc dx, dec dx is used to test the top (sign) bit of dx
jns upkr_load_bit
movzx ax, byte [bx] ; u16 prob = upkr_probs[context_index]
neg byte [bx]
push ax ; save prob, tmp = prob
cmp dl, al ; int bit = (upkr_state & 255) < prob ? 1 : 0; (carry = bit)
pushf ; save bit flags
jc .bit ; (skip if bit)
xchg [bx], al ; tmp = 256 - tmp;
.bit:
shr byte [bx], 4 ; upkr_probs[context_index] = tmp + (256 - tmp + 8) >> 4;
adc [bx], al
mul dh ; upkr_state = tmp * (upkr_state >> 8) + (upkr_state & 255);
mov dh, 0
add dx, ax
popf
pop ax
jc .bit2 ; (skip if bit)
neg byte [bx] ; tmp = 256 - tmp;
sub dx, ax ; upkr_state -= prob; note that this will also leave carry always unset, which is what we want
.bit2:
ret ; flags = bit
; upkr_decode_number loads a variable length encoded number (up to 16 bits) from
; the compressed stream. Only numbers 1..65535 can be encoded. If the encoded
; number has 4 bits and is 1ABC, it is encoded using a kind of an "interleaved
; elias code": 0A0B0C1. The 1 in the end implies that no more bits are coming.
; parameters:
; cx = must be 0
; bx = memory address of the context probability
; dx = decoder state
; bp = bit position in input stream
; carry = must be 1
; returns:
; cx = length
; dx = new decoder state
; bp = new bit position in input stream
; carry = 1
; trashes bl, ax
upkr_decode_number_loop:
inc bx
call upkr_decode_bit
upkr_decode_number:
rcr cx, 1
inc bx
call upkr_decode_bit
jnc upkr_decode_number_loop ; while(upkr_decode_bit(context_index)) {
.loop2:
rcr cx, 1
jnc .loop2
ret
compressed_data:
incbin "data.bin"

View File

@@ -0,0 +1,154 @@
; Contributions from pestis, TomCat and exoticorn
;
; This is the 16-bit DOS x86 decompression stub for upkr, which is designed for
; the --no-repeated-offsets option of upkr. The decompression stub is slightly
; smaller, but the compressed data might be bigger, so you have to test if
; --no-repeated-offsets pays off in the end. This stub relocates the compressed
; data so it can be decompressed starting at the normal .COM starting address.
;
; How to use:
; 1) Pack your intro using upkr into data.bin with the --x86b command line
; argument: (notice the --x86b, not --x86!)
;
; $ upkr --x86b intro.com data.bin
;
; 2) Compile this .asm file using nasm (or any compatible assembler):
;
; $ nasm unpack_x86_16_DOS_no_repeated_offsets.asm -fbin -o intropck.com
;
; The packed size of the intro+stub is limited by max_len (see below) bytes.
;
; In specific cases, the unpacker stub can be further optimized to save a byte
; or two:
; 1) You can remove CLC before RET, if you don't mind carry being set upon
; program entry
; 2) You can also move PUSHA before PUSH SI and put POPA as the first
; operation of the compressed code.
max_len equ 16384
prog_start equ (0x100+max_len+510+relocation-upkr_unpack)
probs equ (((prog_start+max_len+510)+255)/256)*256
org 0x100
; This is will be loaded at 0x100, but relocates the code and data to prog_start
relocation:
push si ; si = 0x100 at DOS start, so save it for later ret
pusha ; pusha to recall all registers before starting intro
push si ; for pop di to start writing the output
mov di, prog_start ; the depacker & data are relocated from 0x100 to prog_start
mov ch, max_len/512
rep movsw
jmp si ; jump to relocated upkr_unpack
; upkr_unpack unpacks the code to 0x100 and runs it when done.
upkr_unpack:
xchg ax, bp ; position in bitstream = 0
cwd ; upkr_state = 0;
xchg cx, ax ; cx > 0x0200
mov al, 128 ; for(int i = 0; i < sizeof(upkr_probs); ++i) upkr_probs[i] = 128;
rep stosb
pop di ; u8* write_ptr = (u8*)destination;
.mainloop:
mov bx, probs
call upkr_decode_bit
jnc .else ; if(upkr_decode_bit(0)) {
inc bh
call upkr_decode_number ; offset = upkr_decode_number(258) - 1;
loop .notdone ; if(offset == 0)
popa
clc
ret
.notdone:
mov si, di
.sub:
dec si
loop .sub
mov bl, 128 ; int length = upkr_decode_number(384);
call upkr_decode_number
rep movsb ; *write_ptr = write_ptr[-offset];
jmp .mainloop
.else:
inc bx
.byteloop:
call upkr_decode_bit ; int bit = upkr_decode_bit(byte);
adc bl, bl ; byte = (byte << 1) + bit;
jnc .byteloop
xchg ax, bx
stosb
jmp .mainloop ; prev_was_match = 0;
; upkr_decode_bit decodes one bit from the rANS entropy encoded bit stream.
; parameters:
; bx = memory address of the context probability
; dx = decoder state
; bp = bit position in input stream
; returns:
; dx = new decoder state
; bp = new bit position in input stream
; carry = bit
; trashes ax
upkr_load_bit:
bt [compressed_data-relocation+prog_start], bp
inc bp
adc dx, dx
upkr_decode_bit:
inc dx
dec dx ; or whatever other test for the top bit there is
jns upkr_load_bit
movzx ax, byte [bx] ; u16 prob = upkr_probs[context_index]
neg byte [bx]
push ax ; save prob, tmp = prob
cmp dl, al ; int bit = (upkr_state & 255) < prob ? 1 : 0; (carry = bit)
pushf ; save bit flags
jc .bit ; (skip if bit)
xchg [bx], al ; tmp = 256 - tmp;
.bit:
shr byte [bx], 4 ; upkr_probs[context_index] = tmp + (256 - tmp + 8) >> 4;
adc [bx], al ; upkr_probs[context_index] = tmp;
mul dh ; upkr_state = tmp * (upkr_state >> 8) + (upkr_state & 255);
mov dh, 0
add dx, ax
popf
pop ax
jc .bit2 ; (skip if bit)
neg byte [bx] ; tmp = 256 - tmp;
sub dx, ax ; upkr_state -= prob; note that this will also leave carry always unset, which is what we want
.bit2:
ret ; flags = bit
; upkr_decode_number loads a variable length encoded number (up to 16 bits) from
; the compressed stream. Only numbers 1..65535 can be encoded. If the encoded
; number has 4 bits and is 1ABC, it is encoded using a kind of an "interleaved
; elias code": 0A0B0C1. The 1 in the end implies that no more bits are coming.
; parameters:
; cx = must be 0
; bx = memory address of the context probability
; dx = decoder state
; bp = bit position in input stream
; carry = must be 1
; returns:
; cx = length
; dx = new decoder state
; bp = new bit position in input stream
; carry = 1
; trashes bl, ax
upkr_decode_number_loop:
inc bx
call upkr_decode_bit
upkr_decode_number:
rcr cx, 1
inc bx
call upkr_decode_bit
jnc upkr_decode_number_loop ; 0 = there's more bits coming, 1 = no more bits
.loop2:
rcr cx, 1
jnc .loop2
ret
compressed_data:
incbin "data.bin"

1
release/.gitignore vendored
View File

@@ -2,3 +2,4 @@
*.tgz
upkr-linux/
upkr-windows/
upkr-windows-32/

View File

@@ -1,11 +1,12 @@
VERSION := $(shell cargo run --release -- --version)
all: clean upkr-linux-$(VERSION).tgz upkr-windows-$(VERSION).zip
all: clean upkr-linux-$(VERSION).tgz upkr-windows-$(VERSION).zip upkr-windows-32-$(VERSION).zip
clean:
rm -rf upkr-linux
rm -f upkr-linux*.tgz
rm -rf upkr-windows
rm -rf upkr-windows-32
rm -f upkr-windows*.zip
upkr-linux-$(VERSION).tgz: upkr-linux/upkr PHONY
@@ -22,14 +23,29 @@ upkr-windows-$(VERSION).zip: upkr-windows/upkr.exe PHONY
cd .. && git archive HEAD asm_unpackers | tar -xC release/upkr-windows
zip -r -9 $@ upkr-windows
upkr-windows-32-$(VERSION).zip: upkr-windows-32/upkr.exe PHONY
cp ../README.md upkr-windows-32/
cd .. && git archive HEAD c_unpacker | tar -xC release/upkr-windows-32
cd .. && git archive HEAD z80_unpacker | tar -xC release/upkr-windows-32
cd .. && git archive HEAD asm_unpackers | tar -xC release/upkr-windows-32
zip -r -9 $@ upkr-windows-32
upkr-linux/upkr:
cargo build --target x86_64-unknown-linux-musl --release
cargo build --target x86_64-unknown-linux-musl --release -F terminal
mkdir -p upkr-linux
cp ../target/x86_64-unknown-linux-musl/release/upkr upkr-linux/
strip upkr-linux/upkr
upkr-windows/upkr.exe:
cargo build --target x86_64-pc-windows-gnu --release
cargo build --target x86_64-pc-windows-gnu --release -F terminal
mkdir -p upkr-windows
cp ../target/x86_64-pc-windows-gnu/release/upkr.exe upkr-windows/
x86_64-w64-mingw32-strip upkr-windows/upkr.exe
upkr-windows-32/upkr.exe:
cargo build --target i686-pc-windows-gnu --release -F terminal
mkdir -p upkr-windows-32
cp ../target/i686-pc-windows-gnu/release/upkr.exe upkr-windows-32/
i686-w64-mingw32-strip upkr-windows-32/upkr.exe
PHONY:
PHONY:

208
src/heatmap.rs Normal file
View File

@@ -0,0 +1,208 @@
/// Heatmap information about a compressed block of data.
///
/// For each byte in the uncompressed data, the heatmap provides two pieces of intormation:
/// 1. whether this byte was encoded as a literal or as part of a match
/// 2. how many (fractional) bits where spend on encoding this byte
///
/// For the sake of the heatmap, the cost of literals are spread out across all matches
/// that reference the literal.
///
/// If the `terminal` feature is enabled, there is a function to write out the
/// heatmap as a colored hexdump.
pub struct Heatmap {
data: Vec<u8>,
cost: Vec<f32>,
raw_cost: Vec<f32>,
literal_index: Vec<usize>,
}
impl Heatmap {
pub(crate) fn new() -> Heatmap {
Heatmap {
data: Vec::new(),
cost: Vec::new(),
raw_cost: Vec::new(),
literal_index: Vec::new(),
}
}
pub(crate) fn add_literal(&mut self, byte: u8, cost: f32) {
self.data.push(byte);
self.cost.push(cost);
self.literal_index.push(self.literal_index.len());
}
pub(crate) fn add_match(&mut self, offset: usize, length: usize, mut cost: f32) {
cost /= length as f32;
for _ in 0..length {
self.data.push(self.data[self.data.len() - offset]);
self.literal_index
.push(self.literal_index[self.literal_index.len() - offset]);
self.cost.push(cost);
}
}
pub(crate) fn finish(&mut self) {
self.raw_cost = self.cost.clone();
let mut ref_count = vec![0usize; self.literal_index.len()];
for &index in &self.literal_index {
ref_count[index] += 1;
}
let mut shifted = vec![];
for (&index, &cost) in self.literal_index.iter().zip(self.cost.iter()) {
let delta = (self.cost[index] - cost) / ref_count[index] as f32;
shifted.push(delta);
shifted[index] -= delta;
}
for (cost, delta) in self.cost.iter_mut().zip(shifted.into_iter()) {
*cost += delta;
}
}
/// Reverses the heatmap
pub fn reverse(&mut self) {
self.data.reverse();
self.cost.reverse();
self.literal_index.reverse();
for index in self.literal_index.iter_mut() {
*index = self.data.len() - *index;
}
}
/// The number of (uncompressed) bytes of data in this heatmap
pub fn len(&self) -> usize {
self.cost.len()
}
/// Returns whether the byte at `index` was encoded as a literal
pub fn is_literal(&self, index: usize) -> bool {
self.literal_index[index] == index
}
/// Returns the cost of encoding the byte at `index` in (fractional) bits.
/// The cost of literal bytes is spread across the matches that reference it.
/// See `raw_cost` for the raw encoding cost of each byte.
pub fn cost(&self, index: usize) -> f32 {
self.cost[index]
}
/// Returns the raw cost of encoding the byte at `index` in (fractional) bits
pub fn raw_cost(&self, index: usize) -> f32 {
self.raw_cost[index]
}
/// Returns the uncompressed data byte at `index`
pub fn byte(&self, index: usize) -> u8 {
self.data[index]
}
#[cfg(feature = "crossterm")]
/// Print the heatmap as a colored hexdump
pub fn print_as_hex(&self) -> std::io::Result<()> {
self.print_as_hex_internal(false)
}
#[cfg(feature = "crossterm")]
/// Print the heatmap as a colored hexdump, based on `raw_cost`.
pub fn print_as_hex_raw_cost(&self) -> std::io::Result<()> {
self.print_as_hex_internal(true)
}
#[cfg(feature = "crossterm")]
fn print_as_hex_internal(&self, report_raw_cost: bool) -> std::io::Result<()> {
use crossterm::{
style::{Attribute, Color, Print, SetAttribute, SetBackgroundColor},
QueueableCommand,
};
use std::io::{stdout, Write};
fn set_color(
mut out: impl QueueableCommand,
heatmap: &Heatmap,
index: usize,
num_colors: u16,
report_raw_cost: bool,
) -> std::io::Result<()> {
let cost = if report_raw_cost {
heatmap.raw_cost(index)
} else {
heatmap.cost(index)
};
if num_colors < 256 {
let colors = [
Color::Red,
Color::Yellow,
Color::Green,
Color::Cyan,
Color::Blue,
Color::DarkBlue,
Color::Black,
];
let color_index = (3. - cost.log2())
.round()
.max(0.)
.min((colors.len() - 1) as f32) as usize;
out.queue(SetBackgroundColor(colors[color_index]))?;
} else {
let colors = [
196, 166, 136, 106, 76, 46, 41, 36, 31, 26, 21, 20, 19, 18, 17, 16,
];
let color_index = ((3. - cost.log2()) * 2.5)
.round()
.max(0.)
.min((colors.len() - 1) as f32) as usize;
out.queue(SetBackgroundColor(Color::AnsiValue(colors[color_index])))?;
}
out.queue(SetAttribute(if heatmap.is_literal(index) {
Attribute::Underlined
} else {
Attribute::NoUnderline
}))?;
Ok(())
}
let num_colors = crossterm::style::available_color_count();
let term_width = crossterm::terminal::size()?.0.min(120) as usize;
let bytes_per_row = (term_width - 8) / 4;
for row_start in (0..self.data.len()).step_by(bytes_per_row) {
let row_range = row_start..self.data.len().min(row_start + bytes_per_row);
let mut stdout = stdout();
stdout.queue(Print(&format!("{:04x} ", row_start)))?;
for i in row_range.clone() {
set_color(&mut stdout, self, i, num_colors, report_raw_cost)?;
stdout.queue(Print(&format!("{:02x} ", self.data[i])))?;
}
let num_spaces = 1 + (bytes_per_row - (row_range.end - row_range.start)) * 3;
let gap: String = std::iter::repeat(' ').take(num_spaces).collect();
stdout
.queue(SetAttribute(Attribute::Reset))?
.queue(Print(&gap))?;
for i in row_range.clone() {
set_color(&mut stdout, self, i, num_colors, report_raw_cost)?;
let byte = self.data[i];
if byte >= 32 && byte < 127 {
stdout.queue(Print(format!("{}", byte as char)))?;
} else {
stdout.queue(Print("."))?;
}
}
stdout
.queue(SetAttribute(Attribute::Reset))?
.queue(Print("\n"))?;
stdout.flush()?;
}
Ok(())
}
}

View File

@@ -1,31 +1,75 @@
#![deny(missing_docs)]
//! Compression and decompression of the upkr format and variants.
//!
//! Upkr is a compression format initially designed for the MicroW8 fantasy console,
//! with design goals being a competitive compression ratio, reasonable fast
//! decompression, low memory overhead and very small decompression code
//! when handoptimized in assembler. (An optimized DOS execuable decompressor is <140 bytes.)
mod context_state;
mod greedy_packer;
mod heatmap;
mod lz;
mod match_finder;
mod parsing_packer;
mod rans;
pub use lz::{calculate_margin, unpack, UnpackError};
pub use heatmap::Heatmap;
pub use lz::{calculate_margin, create_heatmap, unpack, UnpackError};
/// The type of a callback function to be given to the `pack` function.
///
/// It will be periodically called with the number of bytes of the input already processed.
pub type ProgressCallback<'a> = &'a mut dyn FnMut(usize);
/// A configuration of which compression format variation to use.
///
/// Use `Config::default()` for the standard upkr format.
///
/// Compression format variants exist to help with micro-optimizations in uncompression
/// code on specific platforms.
#[derive(Debug)]
pub struct Config {
/// Shift in bits from a bitstream into the rANS state, rather than whole bytes.
/// This decreases the size of the rNAS state to 16 bits which is very useful on
/// 8 bit platforms.
pub use_bitstream: bool,
/// The number of parity contexts (usually 1, 2 or 4). This can improve compression
/// on data that consists of regular groups of 2 or 4 bytes. One example is 32bit ARM
/// code, where each instruction is 4 bytes, so `parity_contexts = 4` improves compression
/// quite a bit. Defaults to `1`.
pub parity_contexts: usize,
/// Invert the encoding of bits in the rANS coder. `bit = state_lo >= prob` instead of
/// `bit = state_lo < prob`.
pub invert_bit_encoding: bool,
/// The boolean value which encodes a match. Defaults to `true`.
pub is_match_bit: bool,
/// The boolean value which encodes a new offset (rather than re-using the previous offset).
/// Defaults to `true`.
pub new_offset_bit: bool,
/// The boolean value which encodes that there are more bits comming for length/offset values.
/// Defaults to `true`.
pub continue_value_bit: bool,
/// Reverses the bits in the bitstream.
pub bitstream_is_big_endian: bool,
/// A slightly less accurate, but slightly simpler variation of the prob update in the
/// rANS coder, Used for the z80 uncompressor.
pub simplified_prob_update: bool,
/// Disables support for re-using the last offset in the compression format.
/// This might save a few bytes when working with very small data.
pub no_repeated_offsets: bool,
/// Standard upkr encodes the EOF marker in the offset. This encodes it in the match length
/// instead.
pub eof_in_length: bool,
/// The maximum match offset value to encode when compressing.
pub max_offset: usize,
/// The maximum match length value to encode when compressing.
pub max_length: usize,
}
@@ -53,7 +97,7 @@ impl Default for Config {
}
impl Config {
pub fn min_length(&self) -> usize {
fn min_length(&self) -> usize {
if self.eof_in_length {
2
} else {
@@ -62,6 +106,21 @@ impl Config {
}
}
/// Compresses the given data.
///
/// # Arguments
/// - `data`: The data to compress
/// - `level`: The compression level (0-9). Increasing the level by one roughly halves the
/// compression speed.
/// - `config`: The compression format variant to use.
/// - `progress_callback`: An optional callback which will periodically be called with
/// the number of bytes already processed.
///
/// # Example
/// ```rust
/// let compressed_data = upkr::pack(b"Hello, World! Yellow world!", 0, &upkr::Config::default(), None);
/// assert!(compressed_data.len() < 27);
/// ```
pub fn pack(
data: &[u8],
level: u8,
@@ -75,6 +134,9 @@ pub fn pack(
}
}
/// Estimate the exact (fractional) size of upkr compressed data.
///
/// Note that this currently does NOT work for the bitstream variant.
pub fn compressed_size(mut data: &[u8]) -> f32 {
let mut state = 0;
while state < 4096 {

View File

@@ -1,4 +1,5 @@
use crate::context_state::ContextState;
use crate::heatmap::Heatmap;
use crate::rans::{EntropyCoder, RansDecoder};
use crate::Config;
use thiserror::Error;
@@ -132,42 +133,91 @@ impl CoderState {
}
}
/// The error type for the uncompressing related functions
#[derive(Error, Debug)]
pub enum UnpackError {
/// a match offset pointing beyond the start of the unpacked data was encountered
#[error("match offset out of range: {offset} > {position}")]
OffsetOutOfRange { offset: usize, position: usize },
OffsetOutOfRange {
/// the match offset
offset: usize,
/// the current position in the uncompressed stream
position: usize,
},
/// The passed size limit was exceeded
#[error("Unpacked data over size limit: {size} > {limit}")]
OverSize { size: usize, limit: usize },
OverSize {
/// the size of the uncompressed data
size: usize,
/// the size limit passed into the function
limit: usize,
},
/// The end of the packed data was reached without an encoded EOF marker
#[error("Unexpected end of input data")]
UnexpectedEOF {
#[from]
/// the underlying EOF error in the rANS decoder
source: crate::rans::UnexpectedEOF,
},
/// An offset or length value was found that exceeded 32bit
#[error("Overflow while reading value")]
ValueOverflow,
}
/// Uncompress a piece of compressed data
///
/// Returns either the uncompressed data, or an `UnpackError`
///
/// # Parameters
///
/// - `packed_data`: the compressed data
/// - `config`: the exact compression format config used to compress the data
/// - `max_size`: the maximum size of uncompressed data to return. When this is exceeded,
/// `UnpackError::OverSize` is returned
pub fn unpack(
packed_data: &[u8],
config: &Config,
max_size: usize,
) -> Result<Vec<u8>, UnpackError> {
let mut result = vec![];
let _ = unpack_internal(Some(&mut result), packed_data, config, max_size)?;
let _ = unpack_internal(Some(&mut result), None, packed_data, config, max_size)?;
Ok(result)
}
/// Calculates the minimum margin when overlapping buffers.
///
/// Returns the minimum margin needed between the end of the compressed data and the
/// end of the uncompressed data when overlapping the two buffers to save on RAM.
pub fn calculate_margin(packed_data: &[u8], config: &Config) -> Result<isize, UnpackError> {
unpack_internal(None, packed_data, config, usize::MAX)
unpack_internal(None, None, packed_data, config, usize::MAX)
}
pub fn unpack_internal(
/// Calculates a `Heatmap` from compressed data.
///
/// # Parameters
///
/// - `packed_data`: the compressed data
/// - `config`: the exact compression format config used to compress the data
/// - `max_size`: the maximum size of the heatmap to return. When this is exceeded,
/// `UnpackError::OverSize` is returned
pub fn create_heatmap(
packed_data: &[u8],
config: &Config,
max_size: usize,
) -> Result<Heatmap, UnpackError> {
let mut heatmap = Heatmap::new();
let _ = unpack_internal(None, Some(&mut heatmap), packed_data, config, max_size)?;
Ok(heatmap)
}
fn unpack_internal(
mut result: Option<&mut Vec<u8>>,
mut heatmap: Option<&mut Heatmap>,
packed_data: &[u8],
config: &Config,
max_size: usize,
) -> Result<isize, UnpackError> {
let mut decoder = RansDecoder::new(packed_data, &config);
let mut decoder = RansDecoder::new(packed_data, &config)?;
let mut contexts = ContextState::new((1 + 255) * config.parity_contexts + 1 + 64 + 64, &config);
let mut offset = usize::MAX;
let mut position = 0usize;
@@ -198,6 +248,7 @@ pub fn unpack_internal(
}
loop {
let prev_decoder = decoder.clone();
margin = margin.max(position as isize - decoder.pos() as isize);
let literal_base = position % config.parity_contexts * 256;
if decoder.decode_with_context(&mut contexts.context_mut(literal_base))?
@@ -231,6 +282,9 @@ pub fn unpack_internal(
if offset > position {
return Err(UnpackError::OffsetOutOfRange { offset, position });
}
if let Some(ref mut heatmap) = heatmap {
heatmap.add_match(offset, length, decoder.cost(&prev_decoder));
}
if let Some(ref mut result) = result {
for _ in 0..length {
if result.len() < max_size {
@@ -251,6 +305,9 @@ pub fn unpack_internal(
context_index = (context_index << 1) | bit as usize;
byte |= (bit as u8) << i;
}
if let Some(ref mut heatmap) = heatmap {
heatmap.add_literal(byte, decoder.cost(&prev_decoder));
}
if let Some(ref mut result) = result {
if result.len() < max_size {
result.push(byte);
@@ -261,6 +318,10 @@ pub fn unpack_internal(
}
}
if let Some(heatmap) = heatmap {
heatmap.finish();
}
if position > max_size {
return Err(UnpackError::OverSize {
size: position,

View File

@@ -9,6 +9,10 @@ fn main() -> Result<()> {
let mut reverse = false;
let mut unpack = false;
let mut calculate_margin = false;
let mut create_heatmap = false;
let mut report_raw_cost = false;
#[allow(unused_mut)]
let mut do_hexdump = false;
let mut level = 2;
let mut infile: Option<PathBuf> = None;
let mut outfile: Option<PathBuf> = None;
@@ -56,8 +60,12 @@ fn main() -> Result<()> {
level = 9;
}
Short('u') | Long("unpack") => unpack = true,
Short('u') | Long("unpack") | Short('d') | Long("decompress") => unpack = true,
Long("margin") => calculate_margin = true,
Long("heatmap") => create_heatmap = true,
Long("raw-cost") => report_raw_cost = true,
#[cfg(feature = "crossterm")]
Long("hexdump") => do_hexdump = true,
Short('l') | Long("level") => level = parser.value()?.parse()?,
Short(n) if n.is_ascii_digit() => level = n as u8 - b'0',
Short('h') | Long("help") => print_help(0),
@@ -72,64 +80,52 @@ fn main() -> Result<()> {
}
}
let infile = infile.unwrap_or_else(|| print_help(1));
let outfile = outfile.unwrap_or_else(|| {
let mut name = infile.clone();
if unpack {
if name.extension().filter(|&e| e == "upk").is_some() {
name.set_extension("");
} else {
name.set_extension("bin");
}
} else {
let mut filename = name
.file_name()
.unwrap_or_else(|| OsStr::new(""))
.to_os_string();
filename.push(".upk");
name.set_file_name(filename);
}
name
});
let infile = IoTarget::from_filename(infile);
let outfile = |tpe: OutFileType| infile.output(tpe, &outfile);
if config.parity_contexts != 1 && config.parity_contexts != 2 && config.parity_contexts != 4 {
eprintln!("--parity has to be 1, 2, or 4");
process::exit(1);
}
if !unpack && !calculate_margin {
let mut data = vec![];
File::open(infile)?.read_to_end(&mut data)?;
if !unpack && !calculate_margin && !create_heatmap {
let mut data = infile.read()?;
if reverse {
data.reverse();
}
let mut pb = pbr::ProgressBar::new(data.len() as u64);
pb.set_units(pbr::Units::Bytes);
let mut packed_data = upkr::pack(
&data,
level,
&config,
Some(&mut |pos| {
pb.set(pos as u64);
}),
);
pb.finish();
#[cfg(feature = "terminal")]
let mut packed_data = {
let mut pb = pbr::ProgressBar::on(std::io::stderr(), data.len() as u64);
pb.set_units(pbr::Units::Bytes);
let packed_data = upkr::pack(
&data,
level,
&config,
Some(&mut |pos| {
pb.set(pos as u64);
}),
);
pb.finish();
eprintln!();
packed_data
};
#[cfg(not(feature = "terminal"))]
let mut packed_data = upkr::pack(&data, level, &config, None);
if reverse {
packed_data.reverse();
}
println!(
eprintln!(
"Compressed {} bytes to {} bytes ({}%)",
data.len(),
packed_data.len(),
packed_data.len() as f32 * 100. / data.len() as f32
);
File::create(outfile)?.write_all(&packed_data)?;
outfile(OutFileType::Packed).write(&packed_data)?;
} else {
let mut data = vec![];
File::open(infile)?.read_to_end(&mut data)?;
let mut data = infile.read()?;
if reverse {
data.reverse();
}
@@ -138,7 +134,36 @@ fn main() -> Result<()> {
if reverse {
unpacked_data.reverse();
}
File::create(outfile)?.write_all(&unpacked_data)?;
outfile(OutFileType::Unpacked).write(&unpacked_data)?;
}
if create_heatmap {
let mut heatmap = upkr::create_heatmap(&data, &config, max_unpacked_size)?;
if reverse {
heatmap.reverse();
}
match do_hexdump {
#[cfg(feature = "crossterm")]
true => {
if report_raw_cost {
heatmap.print_as_hex_raw_cost()?
} else {
heatmap.print_as_hex()?
}
}
_ => {
let mut heatmap_bin = Vec::with_capacity(heatmap.len());
for i in 0..heatmap.len() {
let cost = if report_raw_cost {
heatmap.raw_cost(i)
} else {
heatmap.cost(i)
};
let cost = (cost.log2() * 8. + 64.).round().max(0.).min(127.) as u8;
heatmap_bin.push((cost << 1) | heatmap.is_literal(i) as u8);
}
outfile(OutFileType::Heatmap).write(&heatmap_bin)?;
}
}
}
if calculate_margin {
println!("{}", upkr::calculate_margin(&data, &config)?);
@@ -148,17 +173,102 @@ fn main() -> Result<()> {
Ok(())
}
enum OutFileType {
Packed,
Unpacked,
Heatmap,
}
enum IoTarget {
StdInOut,
File(PathBuf),
}
impl IoTarget {
fn from_filename(filename: Option<PathBuf>) -> IoTarget {
if let Some(path) = filename {
if path.as_os_str() == "-" {
IoTarget::StdInOut
} else {
IoTarget::File(path)
}
} else {
IoTarget::StdInOut
}
}
fn read(&self) -> Result<Vec<u8>> {
let mut buffer = vec![];
match *self {
IoTarget::StdInOut => std::io::stdin().read_to_end(&mut buffer)?,
IoTarget::File(ref path) => File::open(path)?.read_to_end(&mut buffer)?,
};
Ok(buffer)
}
fn write(&self, data: &[u8]) -> Result<()> {
match *self {
IoTarget::StdInOut => std::io::stdout().write_all(data)?,
IoTarget::File(ref path) => File::create(path)?.write_all(data)?,
};
Ok(())
}
fn output(&self, tpe: OutFileType, outname: &Option<PathBuf>) -> IoTarget {
if outname.is_some() {
return IoTarget::from_filename(outname.clone());
}
match *self {
IoTarget::StdInOut => IoTarget::StdInOut,
IoTarget::File(ref path) => {
let mut name = path.clone();
match tpe {
OutFileType::Packed => {
let mut filename = name
.file_name()
.unwrap_or_else(|| OsStr::new(""))
.to_os_string();
filename.push(".upk");
name.set_file_name(filename);
}
OutFileType::Unpacked => {
if name.extension().filter(|&e| e == "upk").is_some() {
name.set_extension("");
} else {
name.set_extension("bin");
}
}
OutFileType::Heatmap => {
name.set_extension("heatmap");
}
}
IoTarget::File(name)
}
}
}
}
fn print_help(exit_code: i32) -> ! {
eprintln!("Usage:");
eprintln!(" upkr [-l level(0-9)] [config options] <infile> [<outfile>]");
eprintln!(" upkr -u [config options] <infile> [<outfile>]");
eprintln!(" upkr --heatmap [config options] <infile> [<outfile>]");
eprintln!(" upkr --margin [config options] <infile>");
eprintln!();
eprintln!(" -l, --level N compression level 0-9");
eprintln!(" -0, ..., -9 short form for setting compression level");
eprintln!(" -u, --unpack unpack infile");
eprintln!(" -d, --decompress decompress infile");
eprintln!(" --heatmap calculate heatmap from compressed file");
eprintln!(" --raw-cost report raw cost of literals in heatmap");
#[cfg(feature = "crossterm")]
eprintln!(" --hexdump print heatmap as colored hexdump");
eprintln!(" --margin calculate margin for overlapped unpacking of a packed file");
eprintln!();
eprintln!("When no infile is given, or the infile is '-', read from stdin.");
eprintln!(
"When no outfile is given and reading from stdin, or when outfile is '-', write to stdout."
);
eprintln!();
eprintln!("Version: {}", env!("CARGO_PKG_VERSION"));
eprintln!();
eprintln!("Config presets for specific unpackers:");

View File

@@ -148,6 +148,7 @@ impl EntropyCoder for CostCounter {
}
}
#[derive(Clone)]
pub struct RansDecoder<'a> {
data: &'a [u8],
pos: usize,
@@ -166,8 +167,8 @@ const PROB_MASK: u32 = ONE_PROB - 1;
pub struct UnexpectedEOF;
impl<'a> RansDecoder<'a> {
pub fn new(data: &'a [u8], config: &Config) -> RansDecoder<'a> {
RansDecoder {
pub fn new(data: &'a [u8], config: &Config) -> Result<RansDecoder<'a>, UnexpectedEOF> {
let mut decoder = RansDecoder {
data,
pos: 0,
state: 0,
@@ -176,7 +177,9 @@ impl<'a> RansDecoder<'a> {
bits_left: 0,
invert_bit_encoding: config.invert_bit_encoding,
bitstream_is_big_endian: config.bitstream_is_big_endian,
}
};
decoder.refill()?;
Ok(decoder)
}
pub fn pos(&self) -> usize {
@@ -189,8 +192,7 @@ impl<'a> RansDecoder<'a> {
Ok(bit)
}
pub fn decode_bit(&mut self, prob: u16) -> Result<bool, UnexpectedEOF> {
let prob = prob as u32;
fn refill(&mut self) -> Result<(), UnexpectedEOF> {
if self.use_bitstream {
while self.state < 32768 {
if self.bits_left == 0 {
@@ -219,6 +221,13 @@ impl<'a> RansDecoder<'a> {
self.pos += 1;
}
}
Ok(())
}
pub fn decode_bit(&mut self, prob: u16) -> Result<bool, UnexpectedEOF> {
self.refill()?;
let prob = prob as u32;
let bit = (self.state & PROB_MASK) < prob;
@@ -231,4 +240,9 @@ impl<'a> RansDecoder<'a> {
Ok(bit ^ self.invert_bit_encoding)
}
pub fn cost(&self, prev: &RansDecoder) -> f32 {
f32::log2(prev.state as f32) - f32::log2(self.state as f32)
+ (self.pos - prev.pos) as f32 * 8.
}
}