Skip to content

Commit

Permalink
Merge branch 'develop' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
winseros committed Dec 23, 2021
2 parents b34fea6 + 3dd40c3 commit 8486048
Show file tree
Hide file tree
Showing 32 changed files with 1,709 additions and 30 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ A tool to open, pack and unpack ArmA PBO files.
- Can preview files inside a PBO.
- Can open mangled (by Mikero's tools) PBO.
- Integrates with Windows Explorer.
- Supports PBO metadata provisioning through [pbo.json](doc/pbo_json.md) or [\$PBOPREFIX\$](doc/prefix_files.md) files.

## Screenshots

Expand Down
88 changes: 88 additions & 0 deletions doc/pbo_json.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Pbo.json

You can put the `pbo.json` file into the PBO content root directory when you pack a PBO via the PBO Manager. The PBO Manager reads the PBO headers and the compression rules from the `pbo.json`.

An example `pbo.json`:

```json
{
"headers": [{
"name": "prefix",
"value": "my-addon"
}, {
"name": "version",
"value": "1.0.0"
}, {
"name": "product",
"value": "my-mod"
}],
"compress": {
"include": ["\.txt$", "\.sqf$", "\.ext"],
"exclude": ["^description.ext$"]
}
}
```

## Pbo.json fields

### headers

The collection of the headers the PBO file should have.

Type: `array`

Required: `no`

Default: `[]`

#### headers[].name

The name of the header.

Type: `string`

Required: `yes`

#### headers[].value

The value of the header.

Type: `string`

Required: `no`

Default: `""`

### compress

The rules of compressing the contents of the PBO file.

#### compress.include

Which files to compress.

Type: `array` of [regular expressions](https://en.wikipedia.org/wiki/Regular_expression). (The regular expressions will run against the **relative** file names with **forward slashes** as separators, e.g. `scripts/script1.sqf`)

Required: `no`

#### compress.exclude

Which of the **included** files to exclude from compression. Use this to _"include all then exclude a couple"_ scenario.

Type: `array` of [regular expressions](https://en.wikipedia.org/wiki/Regular_expression)

Required: `no`


## Regular expression debugging

There are lots of the services for regular expression debugging, such as https://regex101.com

## Regular expression examples

| What it will match | Example files | RegEx |
| ------------------------------------------------- | ------------------------------------------------------------------ | ---------------------- |
| All the `.sqf` files. | `my-script.sqf` or `Scripts/script.sqf` | `\.sqf$` |
| All the `.sqf` files in the `Scripts` root foler. | `Scripts/Client/Alpha/fn_run.sqf` | `^scripts\/.+\.sqf$` |
| All the `.sqf` files in the `Alpha` subfolders. | `Scripts/Client/Alpha/initClient.sqf` or `Scripts/Server/Alpha/initServer.sqf` | `\/.+\/Alpha\/.+\.sqf$` |
| The `decription.ext` file in the root. | `description.ext` | `^description.ext$` |
15 changes: 15 additions & 0 deletions doc/prefix_files.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Prefix files

Since the times of Operation Flashpoint, they used so-called [prefix files](https://community.bistudio.com/wiki/PBOPREFIX) to provide the packaging tools the information regarding the headers a packed PBO should have.

To use a prefix file, you create a text file with the name i.e. `$pboprefix$` (or `pboprefix.txt`) and some text. And when the PBO Manager packs the folder, the produced `PBO` will have the header with the name `prefix` and the text from the file.

The PBO Manager supports the following prefix files (names are case-insensitive):

| File name | Alternative file name | Resulting PBO header |
| -------------- | --------------------- | -------------------- |
| \$pboprefix\$ | pboprefix.txt | prefix |
| \$pboproduct\$ | pboproduct.txt | product |
| \$pboversion\$ | pboversion.txt | version |

However, there is a more efficient way to control file headers: [pbo.json](pbo_json.md).
6 changes: 6 additions & 0 deletions pbom/model/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
list(APPEND PROJECT_SOURCES
"model/task/extractconfiguration.cpp"
"model/task/packconfiguration.cpp"
"model/task/packoptions.cpp"
"model/task/packtask.cpp"
"model/task/packwindowmodel.cpp"
"model/task/task.h"
Expand All @@ -12,6 +15,9 @@ list(APPEND PROJECT_SOURCES
set(PROJECT_SOURCES ${PROJECT_SOURCES} PARENT_SCOPE)

list(APPEND TEST_SOURCES
"model/task/__test__/extractconfiguration_test.cpp"
"model/task/__test__/packconfiguration_test.cpp"
"model/task/__test__/packoptions_test.cpp"
"model/__test__/conflictsparcel_test.cpp"
"model/__test__/interactionparcel_test.cpp")

Expand Down
37 changes: 37 additions & 0 deletions pbom/model/binarysourceutils.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#pragma once

#include "exception.h"
#include "domain/binarysource.h"
#include "io/bs/pbobinarysource.h"
#include "io/bs/fsrawbinarysource.h"
#include "io/bs/fslzhbinarysource.h"

namespace pboman3::model {
using namespace domain;
using namespace io;

inline void ChangeBinarySourceCompressionMode(QSharedPointer<BinarySource>& bs, bool compress) {
if (dynamic_cast<PboBinarySource*>(bs.get())) {
throw InvalidOperationException("Can't query compression status");
}

if (compress) {
if (dynamic_cast<FsRawBinarySource*>(bs.get())) {
bs = QSharedPointer<BinarySource>(new FsLzhBinarySource(bs->path()));
bs->open();
}
} else {
if (dynamic_cast<FsLzhBinarySource*>(bs.get())) {
bs = QSharedPointer<BinarySource>(new FsRawBinarySource(bs->path()));
bs->open();
}
}
}

inline bool IsCompressed(const QSharedPointer<BinarySource>& bs) {
if (const auto pboBs = dynamic_cast<PboBinarySource*>(bs.get())) {
return pboBs->isCompressed();
}
throw InvalidOperationException("Can't query compression status");
}
}
18 changes: 2 additions & 16 deletions pbom/model/interactionparcel.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#include "interactionparcel.h"
#include <QDataStream>
#include <QSet>
#include "exception.h"
#include "binarysourceutils.h"

namespace pboman3::model {
const QSharedPointer<BinarySource>& NodeDescriptor::binarySource() const {
Expand All @@ -13,21 +13,7 @@ namespace pboman3::model {
}

void NodeDescriptor::setCompressed(bool compressed) {
if (dynamic_cast<PboBinarySource*>(binarySource_.get())) {
throw InvalidOperationException("Can't query compression status");
}

if (compressed) {
if (dynamic_cast<FsRawBinarySource*>(binarySource_.get())) {
binarySource_ = QSharedPointer<BinarySource>(new FsLzhBinarySource(binarySource_->path()));
binarySource_->open();
}
} else {
if (dynamic_cast<FsLzhBinarySource*>(binarySource_.get())) {
binarySource_ = QSharedPointer<BinarySource>(new FsRawBinarySource(binarySource_->path()));
binarySource_->open();
}
}
ChangeBinarySourceCompressionMode(binarySource_, compressed);
}

QByteArray NodeDescriptors::serialize(const NodeDescriptors& data) {
Expand Down
158 changes: 158 additions & 0 deletions pbom/model/task/__test__/extractconfiguration_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#include "model/task/extractconfiguration.h"

#include <QTemporaryDir>
#include <QTemporaryFile>
#include <gtest/gtest.h>
#include "domain/pbodocument.h"
#include "io/bs/pbobinarysource.h"

namespace pboman3::model::task {
using namespace domain;

TEST(ExtractConfigurationTest, Extract_Takes_Headers) {
const QList headers{
QSharedPointer<DocumentHeader>(new DocumentHeader("n1", "v1")),
QSharedPointer<DocumentHeader>(new DocumentHeader("n2", "v2")),
QSharedPointer<DocumentHeader>(new DocumentHeader("n3", "")),
};
const PboDocument document("file.pbo", headers, QByteArray(1, 20));

const PackOptions options = ExtractConfiguration::extractFrom(document);

ASSERT_EQ(options.headers.count(), 3);
ASSERT_EQ(options.headers.at(0).name, "n1");
ASSERT_EQ(options.headers.at(0).value, "v1");
ASSERT_EQ(options.headers.at(1).name, "n2");
ASSERT_EQ(options.headers.at(1).value, "v2");
ASSERT_EQ(options.headers.at(2).name, "n3");
ASSERT_EQ(options.headers.at(2).value, "");
}

struct ExtractConfigurationExtParam {
QString fileWithExt;
};

class ExtractConfigurationExtensionTest : public testing::TestWithParam<ExtractConfigurationExtParam> {
};

TEST_P(ExtractConfigurationExtensionTest, Extract_Picks_Extension_Compression) {
const PboDocument document("file.pbo");
document.root()->createHierarchy(PboPath("f1.p3d"));
document.root()->createHierarchy(PboPath("f2.paa"));
document.root()->createHierarchy(PboPath("snd/f3.ogg"));
document.root()->createHierarchy(PboPath(GetParam().fileWithExt));

const PackOptions options = ExtractConfiguration::extractFrom(document);

ASSERT_EQ(options.compress.include.count(), 1);
ASSERT_EQ(options.compress.include.at(0), "\\." + GetFileExtension(GetParam().fileWithExt).toLower() + "$");
}

INSTANTIATE_TEST_SUITE_P(ExtractConfigurationTest, ExtractConfigurationExtensionTest, testing::Values(
ExtractConfigurationExtParam{"file1.sqf"},
ExtractConfigurationExtParam{"folder1/file1.sqf"},
ExtractConfigurationExtParam{"file1.sqs"},
ExtractConfigurationExtParam{"file1.txt"},
ExtractConfigurationExtParam{"file1.Xml"},
ExtractConfigurationExtParam{"file1.cSv"}
));

struct ExtractConfigurationFileParam {
QString fileName;
bool compressed;
};

class ExtractConfigurationFileTest : public testing::TestWithParam<ExtractConfigurationFileParam> {
};

TEST_P(ExtractConfigurationFileTest, Extract_Picks_File_Compression) {
QTemporaryFile file;
file.open();
file.close();

const PboDocument document("file.pbo");
PboNode* node = document.root()->createHierarchy(PboPath(GetParam().fileName));
node->binarySource = QSharedPointer<BinarySource>(
new io::PboBinarySource(file.fileName(), io::PboDataInfo{10, 10, 0, 0, GetParam().compressed}));

const PackOptions options = ExtractConfiguration::extractFrom(document);

if (GetParam().compressed) {
ASSERT_EQ(options.compress.include.count(), 1);
ASSERT_EQ(options.compress.include.at(0), "^" + GetParam().fileName.toLower() + "$");
} else {
ASSERT_EQ(options.compress.include.count(), 0);
}
}

INSTANTIATE_TEST_SUITE_P(ExtractConfigurationTest, ExtractConfigurationFileTest, testing::Values(
ExtractConfigurationFileParam{"mission.sQm", true},
ExtractConfigurationFileParam{"mission.sqm", false},
ExtractConfigurationFileParam{"descriptIon.ext", true},
ExtractConfigurationFileParam{"description.ext", false}
));

TEST(ExtractConfigurationTest, Extract_Picks_Multiple_Compression_Rules) {
QTemporaryFile file;
file.open();
file.close();

const PboDocument document("file.pbo");
PboNode* node = document.root()->createHierarchy(PboPath("mission.sqm"));
node->binarySource = QSharedPointer<BinarySource>(
new io::PboBinarySource(file.fileName(), io::PboDataInfo{10, 10, 0, 0, true}));

document.root()->createHierarchy(PboPath("script.sqf"));

const PackOptions options = ExtractConfiguration::extractFrom(document);

ASSERT_EQ(options.compress.include.count(), 2);
ASSERT_EQ(options.compress.include.at(0), "\\.sqf$");
ASSERT_EQ(options.compress.include.at(1), "^mission.sqm$");
}

TEST(ExtractConfigurationTest, SaveTo_Writes_To_Directory) {
QTemporaryDir t;
const QDir target(t.path());

PackOptions options;
options.headers = QList{PackHeader("h1", "v1"), PackHeader("h2", "v2")};
options.compress.include = QList<QString>{"i1", "i2"};
options.compress.exclude = QList<QString>{"e1", "e2"};

ExtractConfiguration::saveTo(options, target);

QFile config(target.filePath("pbo.json"));
ASSERT_TRUE(config.exists());

ASSERT_TRUE(config.open(QIODeviceBase::ReadOnly));

const QByteArray bytes = config.readAll();
ASSERT_EQ(QString(bytes), QString("{\n \"compress\": {\n \"exclude\": [\n \"e1\",\n \"e2\"\n ],\n \"include\": [\n \"i1\",\n \"i2\"\n ]\n },\n \"headers\": [\n {\n \"name\": \"h1\",\n \"value\": \"v1\"\n },\n {\n \"name\": \"h2\",\n \"value\": \"v2\"\n }\n ]\n}\n"));
}

TEST(ExtractConfigurationTest, SaveTo_Picks_Non_Conflict_Name) {
QTemporaryDir t;
const QDir target(t.path());

//two placeholder files
QFile f1(target.filePath("pbo.json"));
f1.open(QIODeviceBase::NewOnly);
f1.close();
QFile f2(target.filePath("pbo-1.json"));
f2.open(QIODeviceBase::NewOnly);
f2.close();

const PackOptions options;
ExtractConfiguration::saveTo(options, target);

//the resulting file prevented conflicts
QFile config(target.filePath("pbo-2.json"));
ASSERT_TRUE(config.exists());

ASSERT_TRUE(config.open(QIODeviceBase::ReadOnly));

const QByteArray bytes = config.readAll();
ASSERT_EQ(QString(bytes), QString("{\n \"compress\": {\n \"exclude\": [\n ],\n \"include\": [\n ]\n },\n \"headers\": [\n ]\n}\n"));
}
}
Loading

0 comments on commit 8486048

Please sign in to comment.