Skip to content

Commit 78bf4f0

Browse files
authored
Merge pull request #88 from quarkslab/lzma-compression
Add Protobuf LZMA compression for file on disk
2 parents 6841654 + e9ba2a0 commit 78bf4f0

9 files changed

Lines changed: 254 additions & 20 deletions

File tree

.github/workflows/build.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,22 @@ jobs:
407407
path: third_party/idasdk${{ matrix.ida_sdk }}
408408
key: idasdk-${{ runner.os }}-${{ matrix.ida_sdk }}-${{ hashFiles('sdk_lockfile') }}
409409

410+
- name: Install liblzma (Linux)
411+
if: ${{ matrix.os == 'ubuntu-latest' }}
412+
run: |
413+
sudo apt-get update
414+
sudo apt-get install -y liblzma-dev
415+
416+
- name: Install liblzma (MacOS)
417+
if: ${{ matrix.os == 'macos-latest' }}
418+
run: |
419+
brew install xz
420+
421+
- name: Install liblzma (Windows)
422+
if: ${{ matrix.os == 'windows-latest' }}
423+
run: |
424+
vcpkg install liblzma:x64-windows
425+
410426
- name: Prepare build environment (Linux)
411427
if: ${{ matrix.os == 'ubuntu-latest' }}
412428
env:

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,8 @@ if (NOT NO_BUILD)
195195
# Include protobuf functions
196196
include(cmake/protobuf.cmake)
197197

198+
find_package(LibLZMA REQUIRED)
199+
198200
find_package(IdaSdk REQUIRED)
199201

200202
add_subdirectory(proto)

bindings/python/quokka/program.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import sys
2828
from pathlib import Path
2929
from typing import TYPE_CHECKING, Type, Iterable
30+
import lzma
3031

3132
import capstone
3233
import networkx
@@ -107,10 +108,17 @@ def __init__(self, export_file: Path|str, exec_path: Path|str):
107108
"""Constructor"""
108109
super(dict, self).__init__()
109110

110-
self.proto = Pb() # type: ignore
111+
self.proto: quokka.pb.Quokka = quokka.pb.Quokka()
111112
self.export_file: Path = Path(export_file)
112-
with open(self.export_file, "rb") as fd:
113-
self.proto.ParseFromString(fd.read())
113+
try:
114+
with lzma.open(self.export_file, "rb") as fd:
115+
raw_data = fd.read()
116+
except lzma.LZMAError:
117+
# try reading it as a plain-bytes Quokka (but should raise version mismatch later)
118+
with open(self.export_file, "rb") as fd:
119+
raw_data = fd.read()
120+
121+
self.proto.ParseFromString(raw_data)
114122

115123
# Export mode
116124
self.mode: ExporterMode = ExporterMode.from_proto(self.proto.exporter_meta.mode)

ghidra_extension/build.gradle

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def protobufVersion = '4.31.0'
3131

3232
dependencies {
3333
compileOnly "com.google.protobuf:protobuf-java:${protobufVersion}"
34+
implementation 'org.tukaani:xz:1.12'
3435

3536
testImplementation "com.google.protobuf:protobuf-java:${protobufVersion}"
3637
testImplementation 'junit:junit:4.13.2'
@@ -46,6 +47,13 @@ protobuf {
4647
// Ensure proto generation runs before compilation
4748
compileJava.dependsOn 'generateProto'
4849

50+
// Ghidra's copyDependencies copies implementation JARs to lib/.
51+
// The protobuf plugin's extractIncludeProto scans those same JARs,
52+
// so it must run after copyDependencies to avoid implicit dependency errors.
53+
tasks.matching { it.name.startsWith('extractInclude') && it.name.endsWith('Proto') }.configureEach {
54+
dependsOn 'copyDependencies'
55+
}
56+
4957
test {
5058
useJUnit()
5159
}

ghidra_extension/src/main/java/com/quarkslab/quokka/ExportPipeline.java

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
import ghidra.util.task.TaskMonitor;
99
import quokka.QuokkaOuterClass.Quokka;
1010

11+
import org.tukaani.xz.LZMA2Options;
12+
import org.tukaani.xz.XZOutputStream;
13+
1114
import java.io.File;
1215
import java.io.FileOutputStream;
1316
import java.util.Iterator;
@@ -89,17 +92,26 @@ public static void export(Program program, File outputFile,
8992
monitor.setMessage("Quokka: collecting headers...");
9093
builder.setHeaders(collectHeaders(program));
9194

92-
// Phase 8: Serialize
93-
monitor.setMessage("Quokka: writing protobuf...");
95+
// Phase 8: Compress & serialize (LZMA/XZ)
96+
monitor.setMessage("Quokka: compressing & writing protobuf...");
9497
Quokka proto = builder.build();
95-
try (FileOutputStream fos = new FileOutputStream(outputFile)) {
96-
proto.writeTo(fos);
98+
int rawSize = proto.getSerializedSize();
99+
try (FileOutputStream fos = new FileOutputStream(outputFile);
100+
XZOutputStream xzOut = new XZOutputStream(fos, new LZMA2Options())) {
101+
proto.writeTo(xzOut);
97102
}
98103

104+
long compressedSize = outputFile.length();
99105
long elapsed = System.currentTimeMillis() - startTime;
106+
if (rawSize > 0) {
107+
Msg.info(ExportPipeline.class, String.format(
108+
"Compressed %d bytes -> %d bytes (%.1f%%)",
109+
rawSize, compressedSize,
110+
100.0 * (rawSize - compressedSize) / rawSize));
111+
}
100112
Msg.info(ExportPipeline.class, "Quokka export complete in " + elapsed
101113
+ "ms -> " + outputFile.getAbsolutePath()
102-
+ " (" + outputFile.length() + " bytes)");
114+
+ " (" + compressedSize + " bytes)");
103115
}
104116

105117
/**

include/quokka/Logger.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,18 @@ class Logger {
186186
}
187187
}
188188

189+
/**
190+
* Flush all pending log messages
191+
*/
192+
void Flush() {
193+
fflush(stderr);
194+
fflush(stdout);
195+
if (m_defaultui) {
196+
// Force IDA to flush its message buffer
197+
refresh_idaview_anyway();
198+
}
199+
}
200+
189201
/**
190202
* Singleton pattern
191203
* @return An instance to self

include/quokka/LzmaStreambuf.h

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Copyright 2022-2023 Quarkslab
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
/**
16+
* @file LzmaStreambuf.h
17+
* A std::streambuf that LZMA-compresses data on-the-fly.
18+
*/
19+
20+
#ifndef QUOKKA_LZMA_STREAMBUF_H
21+
#define QUOKKA_LZMA_STREAMBUF_H
22+
23+
#include <cstdint>
24+
#include <ostream>
25+
#include <streambuf>
26+
27+
#include <lzma.h>
28+
29+
namespace quokka {
30+
31+
/**
32+
* A std::streambuf that LZMA-compresses data on-the-fly and writes the
33+
* compressed output directly to a destination std::ostream.
34+
*
35+
* Usage:
36+
* std::ofstream file("out.bin", std::ios::binary);
37+
* LzmaStreambuf lzma_buf(file);
38+
* std::ostream lzma_out(&lzma_buf);
39+
* protobuf.SerializeToOstream(&lzma_out);
40+
* lzma_buf.finish(); // flush & finalize the LZMA stream
41+
*/
42+
class LzmaStreambuf : public std::streambuf {
43+
public:
44+
explicit LzmaStreambuf(std::ostream& dest,
45+
uint32_t preset = LZMA_PRESET_DEFAULT)
46+
: dest_(dest), lzma_stream_(LZMA_STREAM_INIT), finished_(false) {
47+
lzma_ret ret = lzma_easy_encoder(&lzma_stream_, preset, LZMA_CHECK_CRC64);
48+
if (ret != LZMA_OK) {
49+
ok_ = false;
50+
return;
51+
}
52+
ok_ = true;
53+
setp(in_buf_, in_buf_ + kBufSize);
54+
}
55+
56+
~LzmaStreambuf() override {
57+
if (!finished_) finish();
58+
lzma_end(&lzma_stream_);
59+
}
60+
61+
// Non-copyable, non-movable
62+
LzmaStreambuf(const LzmaStreambuf&) = delete;
63+
LzmaStreambuf& operator=(const LzmaStreambuf&) = delete;
64+
65+
/// Finalize the LZMA stream (must be called before reading sizes).
66+
bool finish() {
67+
if (finished_) return ok_;
68+
finished_ = true;
69+
// Flush whatever remains in the put-area, then signal LZMA_FINISH.
70+
ok_ = flush_to_lzma(LZMA_FINISH) && ok_;
71+
return ok_;
72+
}
73+
74+
bool ok() const { return ok_; }
75+
uint64_t total_in() const { return lzma_stream_.total_in; }
76+
uint64_t total_out() const { return lzma_stream_.total_out; }
77+
78+
protected:
79+
int overflow(int ch) override {
80+
if (!ok_) return EOF;
81+
// Flush the full buffer first
82+
if (!flush_to_lzma(LZMA_RUN)) {
83+
ok_ = false;
84+
return EOF;
85+
}
86+
// Now the buffer is reset, safe to write the new character
87+
if (ch != EOF) {
88+
*pptr() = static_cast<char>(ch);
89+
pbump(1);
90+
}
91+
return (ch == EOF) ? 0 : ch;
92+
}
93+
94+
int sync() override {
95+
if (!ok_) return -1;
96+
if (!flush_to_lzma(LZMA_RUN)) {
97+
ok_ = false;
98+
return -1;
99+
}
100+
return 0;
101+
}
102+
103+
private:
104+
static constexpr size_t kBufSize = 65535;
105+
106+
bool flush_to_lzma(lzma_action action) {
107+
lzma_stream_.next_in = reinterpret_cast<const uint8_t*>(pbase());
108+
lzma_stream_.avail_in = static_cast<size_t>(pptr() - pbase());
109+
110+
do {
111+
lzma_stream_.next_out = out_buf_;
112+
lzma_stream_.avail_out = kBufSize;
113+
114+
lzma_ret ret = lzma_code(&lzma_stream_, action);
115+
if (ret != LZMA_OK && ret != LZMA_STREAM_END) return false;
116+
117+
size_t have = kBufSize - lzma_stream_.avail_out;
118+
if (have > 0) {
119+
dest_.write(reinterpret_cast<const char*>(out_buf_), have);
120+
if (!dest_) return false;
121+
}
122+
123+
if (ret == LZMA_STREAM_END) break;
124+
} while (lzma_stream_.avail_in > 0 || lzma_stream_.avail_out == 0);
125+
126+
setp(in_buf_, in_buf_ + kBufSize);
127+
return true;
128+
}
129+
130+
std::ostream& dest_;
131+
lzma_stream lzma_stream_;
132+
char in_buf_[kBufSize];
133+
uint8_t out_buf_[kBufSize];
134+
bool ok_;
135+
bool finished_;
136+
};
137+
138+
} // namespace quokka
139+
140+
#endif // QUOKKA_LZMA_STREAMBUF_H

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ ida_target_link_libraries(
6161
absl::strings
6262
absl::str_format
6363
protobuf::libprotobuf
64+
LibLZMA::LibLZMA
6465
)
6566

6667
ida_install(TARGETS quokka_plugin

src/Quokka.cpp

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
#include "quokka/Function.h"
5353
#include "quokka/Layout.h"
5454
#include "quokka/Logger.h"
55+
#include "quokka/LzmaStreambuf.h"
5556
#include "quokka/ProtoWrapper.h"
5657
#include "quokka/Quokka.h"
5758
#include "quokka/Reference.h"
@@ -182,7 +183,8 @@ static int ExportBinary(const std::string& filename) {
182183
}
183184
auto [functions, ranges] = std::move(funcs_and_ranges);
184185

185-
// Export data types (after functions so decompiler-created types are captured)
186+
// Export data types (after functions so decompiler-created types are
187+
// captured)
186188
{
187189
SCOPED_BOX_STEP("quokka: exporting data types",
188190
"Starting to export data types...",
@@ -205,23 +207,56 @@ static int ExportBinary(const std::string& filename) {
205207
WriteFunctions(&quokka_protobuf, std::move(functions));
206208
}
207209

208-
replace_wait_box("quokka: writing on the wire");
210+
replace_wait_box("quokka: compressing & writing");
211+
QLOG_INFO << "Compressing and writing the file...";
209212
std::string outfile = filename;
210213

211-
std::fstream stream(outfile,
212-
std::ios::binary | std::ios::out | std::ios::trunc);
213-
if (!quokka_protobuf.SerializeToOstream(&stream)) {
214-
QLOGE << "Unable to write the file, trying with a temp file.";
215-
outfile = "/tmp/Exported.quokka";
216-
stream = std::fstream(outfile,
217-
std::ios::binary | std::ios::out | std::ios::trunc);
218-
if (!quokka_protobuf.SerializeToOstream(&stream)) {
219-
QLOG_FATAL << "Unable to write to temp file as well";
214+
std::fstream file(outfile,
215+
std::ios::binary | std::ios::out | std::ios::trunc);
216+
if (!file) {
217+
QLOG_ERROR << absl::StrFormat("Failed to open file %s for writing",
218+
outfile);
219+
return false;
220+
}
221+
222+
LzmaStreambuf lzma_buf(file);
223+
std::ostream lzma_out(&lzma_buf);
224+
225+
if (!quokka_protobuf.SerializeToOstream(&lzma_out)) {
226+
// Print internal state for debugging
227+
QLOG_ERROR << "Failed to serialize protobuf to output stream";
228+
QLOG_ERROR << absl::StrFormat(
229+
"Stream state: good=%d, bad=%d, fail=%d, eof=%d", lzma_out.good(),
230+
lzma_out.bad(), lzma_out.fail(), lzma_out.eof());
231+
QLOG_ERROR << absl::StrFormat(
232+
"Underlying file state: good=%d, bad=%d, fail=%d", file.good(),
233+
file.bad(), file.fail());
234+
235+
// Check protobuf message size to see if it exceeds 2GB limit
236+
size_t msg_size = quokka_protobuf.ByteSizeLong();
237+
QLOG_INFO << absl::StrFormat("Protobuf message size: %.2f MB",
238+
msg_size / (1024.0 * 1024.0));
239+
if (msg_size > INT_MAX) {
240+
QLOG_ERROR << "Protobuf message exceeds 2GB serialization limit";
220241
}
242+
243+
return false;
221244
}
222245

246+
if (!lzma_buf.finish()) {
247+
QLOG_ERROR << "Failed to finalize LZMA stream";
248+
return false;
249+
}
250+
251+
uint64_t in_size = lzma_buf.total_in();
252+
uint64_t out_size = lzma_buf.total_out();
253+
254+
QLOG_INFO << absl::StrFormat("Compressed %llu bytes -> %llu bytes (%.1f%%)",
255+
in_size, out_size,
256+
(100.0 * (in_size - out_size) / in_size));
257+
223258
QLOG_INFO << absl::StrFormat("File %s is written", outfile);
224-
QLOG_INFO << absl::StrFormat("quokka finished (took: %.2fs)",
259+
QLOG_INFO << absl::StrFormat("quokka finished (took %.2fs)",
225260
timer.ElapsedSeconds(absl::Now()));
226261

227262
// Clean everything

0 commit comments

Comments
 (0)