Forgot to actually stage changes for last commit...

This commit is contained in:
WrinklyNinja
2013-04-28 15:38:07 +01:00
parent 5780ffb7b6
commit 020d9df688
22 changed files with 232 additions and 1495 deletions
+9 -6
View File
@@ -11,10 +11,12 @@
cmake_minimum_required (VERSION 2.8.9)
project (boss)
set (BOSS_SRC "${CMAKE_SOURCE_DIR}/src/metadata.cpp" "${CMAKE_SOURCE_DIR}/src/game.cpp" "${CMAKE_SOURCE_DIR}/src/helpers.cpp" "${CMAKE_SOURCE_DIR}/src/plugin/ModFormat.cpp" "${CMAKE_SOURCE_DIR}/src/plugin/VersionRegex.cpp")
set (BOSS_SRC "${CMAKE_SOURCE_DIR}/src/metadata.cpp" "${CMAKE_SOURCE_DIR}/src/game.cpp" "${CMAKE_SOURCE_DIR}/src/helpers.cpp" "${CMAKE_SOURCE_DIR}/src/VersionRegex.cpp")
set (BOSS_SRC ${BOSS_SRC} "${PROJECT_LIBS_DIR}/libespm/src/commonSupport.cpp" "${PROJECT_LIBS_DIR}/libespm/src/fileFormat.cpp")
# Include source and library directories.
include_directories ("${PROJECT_LIBS_DIR}/alphanum" "${PROJECT_LIBS_DIR}/utf8" "${PROJECT_LIBS_DIR}/boost" "${PROJECT_LIBS_DIR}/yaml-cpp/include" "${CMAKE_SOURCE_DIR}/src" "${PROJECT_LIBS_DIR}/libloadorder/src")
include_directories ("${PROJECT_LIBS_DIR}/alphanum" "${PROJECT_LIBS_DIR}/utf8" "${PROJECT_LIBS_DIR}/boost" "${PROJECT_LIBS_DIR}/yaml-cpp/include" "${CMAKE_SOURCE_DIR}/src" "${PROJECT_LIBS_DIR}/libloadorder/src" "${PROJECT_LIBS_DIR}/BOSS/trunk" "${PROJECT_LIBS_DIR}/libespm" "${PROJECT_LIBS_DIR}/zlib")
##############################
@@ -39,6 +41,7 @@ ENDIF ()
link_directories ("${PROJECT_LIBS_DIR}/yaml-cpp/build")
link_directories ("${PROJECT_LIBS_DIR}/libloadorder/build")
link_directories ("${PROJECT_LIBS_DIR}/zlib" )
# Settings when compiling and cross-compiling on Linux.
IF (CMAKE_HOST_SYSTEM_NAME MATCHES "Linux")
@@ -53,12 +56,12 @@ IF (CMAKE_HOST_SYSTEM_NAME MATCHES "Linux")
set (BOSS_LIBS version)
ENDIF ()
set (BOSS_LIBS ${BOSS_LIBS} loadorder yaml-cpp boost_filesystem boost_system boost_regex)
set (BOSS_LIBS ${BOSS_LIBS} zlibstatic loadorder yaml-cpp boost_locale boost_filesystem boost_system boost_regex)
ENDIF ()
# Settings when compiling on Windows.
IF (CMAKE_HOST_SYSTEM_NAME MATCHES "Windows")
set (BOSS_LIBS loadorder yaml-cpp libboost_filesystem-vc110-mt-1_52 libboost_system-vc110-mt-1_52)
set (BOSS_LIBS zlibstatic loadorder yaml-cpp boost_locale-vc110-mt-1_52 libboost_filesystem-vc110-mt-1_52 libboost_system-vc110-mt-1_52)
set (CMAKE_CXX_FLAGS "/EHsc")
ENDIF ()
@@ -67,8 +70,8 @@ ENDIF ()
##############################
# Build API.
add_library (boss ${PROJECT_LINK} "${CMAKE_SOURCE_DIR}/src/api.cpp" ${BOSS_SRC})
target_link_libraries (boss ${BOSS_LIBS})
#add_library (boss ${PROJECT_LINK} "${CMAKE_SOURCE_DIR}/src/api.cpp" ${BOSS_SRC})
#target_link_libraries (boss ${BOSS_LIBS})
# Build tester.
add_executable (tester "${CMAKE_SOURCE_DIR}/src/tester.cpp" ${BOSS_SRC})
-248
View File
@@ -1,248 +0,0 @@
LOOT Metadata File Format
==========================
The LOOT metadata file format is a YAML file which contains a specific set of
data in a specific structure. It is used by the masterlist, the userlist and the
lastrun file.
General properties are:
1. Keys and keywords (such as message types, conditional types) are lowercase.
2. Whitespace is important: a two-space indentation should be used between
each level.
3. All YAML properties.
In YAML, the message object is:
---
!message:
condition: OPTIONAL !condition
type: REQUIRED !!string
content: REQUIRED !!string
lang: OPTIONAL !!string
...
Allowed values for the 'type' node are "say", "warn" or "error". The semantics
of the types are:
say Generic message type, carries no semantics.
warn A warning message, detailing a non-critical installation issue.
error An error message, detailing a critical installation issue.
The 'content' node string will treat all file:// , http:// and https:// links as
hyperlinks. A link can be given a label by enclosing it and the label in double
quotes, eg. "http://www.example.com example link".
The 'lang' node takes an ISO 639-3 language string, of which there are a set defined for the languages LOOT supports (as yet, just 'eng').
The file object:
---
!file:
condition: OPTIONAL !condition
name: REQUIRED !!string
display: OPTIONAL !!string
...
The 'display' node allows the name of the parent mod or program to be used instead
of the filename in messages.
The tag object:
---
!tag:
condition: OPTIONAL !condition
name: !!string
...
This object can be used to represent a conditional tag. 'name' is the tag name,
eg. Graphics. Tags that are prepended with a '-' (eg. '-Graphics') are suggested
for removal, otherwise tags are suggested for addition.
The condition string:
---
!condition: REQUIRED !!string
...
Syntax of the string is the same as is currently for BOSS, except the following:
1. The VAR, REGEX and LANG condition types have been removed.
2. The FILE condition type now evaluates regex path strings.
3. The '&&' and '||' operators have been replaced with 'and' and 'or' respectively.
4. Condition keywords, types and operators are all lowercase.
5. Brackets may be used to logically group conditions in compound conditions.
6. Evaluation of compound conditionals is done according to standard operator
precedence rules.
The plugin object:
---
!plugin:
name: REQUIRED !!string
priority: OPTIONAL !undecided
enabled: OPTIONAL !bool
after: OPTIONAL [!file]
req: OPTIONAL [!file]
inc: OPTIONAL [!file]
msg: OPTIONAL [!message]
tag: OPTIONAL [!!string or !tag]
...
The 'name' node can be either an exact filename or a regular expression. The
difference will be determined by looking for the "\.esp" or "\.esm" substrings,
which are only valid for a regular expression.
The type of the 'priority' node has not been decided yet, as it's not yet clear
how much fidelity is required for it.
If any of the 'req' node objects are not present, an error message will be
displayed. If any of the 'incs' node objects are present, an error message will
be displayed. The 'req' node is intended for use with implicit dependencies.
The 'req' node objects must be listed in load order. The 'req' and 'inc'
nodes may contain a mix of plugin and non-plugin objects.
If the 'enabled' node is not specified, its value is assumed true. It is
provided for use in the userlist.
The 'after' node is a list of plugins that the object plugin must load after, intended for use specifying plugins which the object plugin must load after, but which are not dependencies. It may also be used in the userlist to specify custom positions.
The 'tag' node holds an array of Bash Tags to be suggested.
Simple Example
--------------
---
- name: MyMod.esp
req:
- name: AnotherMod.esp
- name: AnotherModPatch.esp
msg:
- type: say
content: "This is my mod."
- type: say
content: "This is a second message."
...
Complex Example
---------------
---
# Note that classes are not specified, YAML will treat them as assoc. arrays,
# LOOT will assume they are file objects.
filevars:
- &GAME name: "TESV.exe"
- &MASTER {name: "Oblivion.esm", ver: "1.2.416"}
- &SE name: "obse_loader.exe"
- &TEST {name: "Test.esp", ver: 0x0537AB3C}
- &SE20 <<: [*SE, ver: "0.0.20.1"]
msgvars:
- &OBSOLETE {type: SAY, content: "Obsolete. Remove and upgrade to the latest version."}
globals:
- {type: SAY, content: "You're using LOOT!"}
- name: Oblivion.esm
msg:
- {condition: exp: "IF FILE("TESV.exe")", type: WARN, content: "False alarm."}
- <<: {*OBSOLETE, content: Quotes aren't necessary, unless the message contains special chars. }
...
LOOT Plugin Data Cache Format
=============================
The formatting of this cache doesn't need to be human-readable, but must
contain the following information for each plugin:
* Filename (for roughly identifying the file)
* CRC (for precisely identifying the exact file)
* Masters in the order they appear in the plugin's master list.
* FormIDs of the records it edits (not added records).
Rather than have a single cache file, it is more efficient past the short
term to use a different file for each plugin filename's data, even after
taking into account the additional networking cost of using many small
files. See the section after the divider below for the analysis.
The cache file extension will be ".dc", an acronym for "data cache". Each
cache file will share the filename of the plugin that it holds the data of,
including the plugin's file extension, eg. "Unofficial Oblivion Patch.esp.dc".
In each cache file, the data will be divided into entries for each of the
different versions of that file, which are handled using their CRCs. These
entries make up the bulk of the file, and are followed by an index of CRCs
and offsets to their corresponding data at the end of the file.
Name Type Details
---------------------------------------------------------------------------
Version uint8_t Version number for the file's
format (in case it changes).
Name Length uint8_t The plugin filename's length.
Filename char[Name Length] Not null terminated.
Data Entries data entry[Entry Count]
Index index entry[Entry Count]
Entry Count uint32_t
Each data entry has the following structure:
Name Type Details
---------------------------------------------------------------------------
Size uint32_t Total size of data entry, not
including this value.
Masters Length uint16_t Includes null characters.
Masters char[Masters Length] Null-terminated filenames in order
of how they appeared in the plugin.
FormIDs uint32_t[len] len = (Size - Masters Length) / 4
Each index entry has the following structure:
Name Type Details
---------------------------------------------------------------------------
File CRC uint32_t
Data offset uint32_t From beginning of file.
All numbers are encoded low to high byte. All strings are encoded in UTF-8.
The reason for this structure is to optimise the processes that will be
performed upon cache files: searching, reading data and appending new data.
The index allows quick searching of the file as it is composed of a given
number of entries with defined size. It is located at the end of the file
rather than the more usual beginning so that appending new data does not
need to rewrite the whole file to also update the index: only the index and
filename data will ever be rewritten.
The plugin filename is contained within the cache file so that in case the
cache file's filename ever gets changed, it is still possible to work out
which plugins' data it holds.
Each cache file will be able to store up to ~10^7 entries, though the
probability of there being a CRC collision reaches 1% at ~10^4 entries, due
to the birthday problem. Still, it's unlikely that there will ever be
several thousand versions of a file with the same filename in use, so it's
probably not worth worrying about.
Single vs. Multi-File
---------------------
A single-file cache would use the above structure, but with an additional
uint64_t in each index entry to store a filename hash, and without the filename
at the start of the file.
Let's say the cache file holds info for 100,000 plugins (25,000 mods, each
with two plugins, and having two versions released - probably still a
conservative estimate). Assuming each plugin changes 10 records, and has 2
masters with filenames 10 characters long, the size is ~11.4 MB.
Using a multi-file cache and the same assumptions being made as above,
the size of each file will be 390 bytes.
However, because we're dealing with lots of small files, HTTP request/response
time has to be accounted for. Google says that the best average for the
upload/download speed ratio of most Internet connections is 1:4, and that
headers are roughly 300 bytes long without cookies. Each file's effective size
is therefore 1890 bytes.
For a load order of 250 plugins, the total effective size is ~461 kB. This is
~25x smaller than the equivalent single-file cache that would need to be
transferred, but it doesn't take into account the HTTP connection time, which
is significant for small files. I don't know how I can take that into account,
but if I've understood things correctly, I don't need to, as a persistent
connection could be used, which would require only one HTTP connection to be
made. HTTP pipelining could also be used to improve performance. A multi-file
cache could also be cached, with new files only being downloaded if they had
changed or weren't already on the user's system, rather than the whole cache
having to be re-downloaded even if the only change was the addition of a plugin
the user doesn't have. This would only save 95 kB over the assumed load order at
maximum though.
Given all that, it's probably better to use a multi-file cache over a single
file cache.
+15 -155
View File
@@ -150,13 +150,12 @@ A general workflow for a user running LOOT to sort would be:
LOOT must also provide a means to manage past user choices, to undo its
changes (only back one run though), and to edit its settings.
LOOT should ask for permission to send plugin contents data on first run, and
to send plugin metadata whenever there are metadata changes to send, unless the
user selects a "do not ask me again" option. These settings should be
configurable at any time.
LOOT should ask for permission to send plugin metadata whenever there are
metadata changes to send, unless the user selects a "do not ask me again"
option. This setting should be configurable at any time.
Documentation will be split between three readmes: one general readme, one API
readme and one rule syntax readme. The version history can be split into a
readme and one syntax readme. The version history can be split into a
fourth readme when it grows sufficiently long.
@@ -183,33 +182,17 @@ LOOT uses specified in a settings file, which will be editable via the GUI.
Because auto-sorting requires LOOT to know what is inside the plugins it sorts,
it must scan the plugins, and determine which of its records are new and which
are edited (new records aren't used for determining load order). However, this
requires significant computation, so LOOT will use a database that holds which
records each plugin changes. The database will be hosted online, and will be
used in the following manner:
are edited (new records aren't used for determining load order).
1. LOOT checks to see if a database update is available, and downloads the
update if so. This may be done by diff or by downloading the whole file if
a diff is not feasible.
2. LOOT scans the installed plugins, calculating the CRC of each plugin.
3. For each plugin, the database is scanned for an entry with a matching file
name and CRC. If an entry is found, the entry's data is used and scanning
of the plugin is skipped.
4. Any plugins which do not already have database entries are scanned.
5. The correct load order of the plugins is calculated using data from scanned
plugins, the database, the masterlist and the userlist.
6. The data from the scanned plugins is appended to the database file, which
is then uploaded to its hosted location.
In addition to having this database file as a sort of permanent and
self-updating cache, optimisations can also be made to skip load order
Optimisations can be made to skip load order
calculation. For example, if the only changes from the last load order set by
LOOT is that a plugin or plugins have been removed, then the last load order
may be used. It might also be possible to only perform partial calculation
when plugins are added, though this is not certain.
when plugins are added, though this is not certain. It might not also be worth
the effort, since preliminary testing shows that scanning is considerably
faster than first anticipated.
Note: For clarity, the database that holds the results of plugin scanning will
be referred to as the "cache". The list of metadata that is maintained by the
Note: For clarity, the he list of metadata that is maintained by the
LOOT team will be referred to as the "masterlist", and the list of metadata that
is individual to each user will be referred to as the "userlist".
@@ -261,59 +244,16 @@ Further details can be found in the 'docs/BOSS Metadata File Syntax.html' file.
Networking
==========
LOOT has four target networking functionalities:
LOOT has two target networking functionalities:
* Remote / local cache sync.
* Masterlist updating.
* User feedback submission.
None of these functionalities are critical to usage of LOOT.
Neither of these functionalities are critical to usage of LOOT.
The masterlist and cache repositories will be Subversion repositories, as this
allows users to most easily download only the files they require, and only the
diffs of those files.
As LOOT needs commit as well as checkout access to the cache repository, for
users without their own credentials, it must access the repository using a
robot account. Google Code does not allow the use of robot accounts, but
Sourceforge does. However, the actions performed by the robot account are the responsibility of the account creator. This brings up the issue of account
abuse. This will be covered in the 'Security' section below.
Conflicts between different users' commits will probably be an issue.
Because the database files are binary, not text, and because they use an index
with data offsets, they can't be merged automatically. The database file
workflow is:
1. User svn updates their working copy of a file.
2. User performs scanning actions.
3. User edits their working copy of the file.
4. User commits their edit to the repository.
If another user changes the same file between steps 1 and 4, the commit will
fail. If this happens, LOOT should svn revert followed by svn update, then
re-apply the user's edits to the updated file, checking to make sure that the
updated file doesn't already include the same data. A second commit should then
be attempted. If this commit also fails, it's best just to discard the edits.
While that means the user won't benefit from having a local cache, it becomes
too complicated to store the edits on disk and re-apply them for a potentially
uncapped number of failures.
The database repository may store tens if not hundreds of thousands of files,
so they need to be organised well to ease browsing. Grouping the files into
the game they are for, then subgrouping them into folders based on the first
character of their filename, then subgrouping again based on the second
character would probably do. For example:
repo/
Oblivion/
o/
b/
oblivion.esm.bpdc
Files and the 'letter folders' will be in lowercase.
"repo" does not have to be the repository root, but LOOT's settings should
use the URL pointing to that location. The repository will be required to use
the same game folder names as LOOT's install structure (see the section below).
The masterlist repository will use Subversion, as this
allows users to most easily download only the file they require, and only the
diffs of that file.
The masterlists would be stored using a folder structure that mirrors LOOT's
install structure, for example:
@@ -345,86 +285,6 @@ update is easily performed through a global masterlist message and so does not
require an additional transport mechanism.
Security
========
The abuse of the database repository credentials to be distributed with LOOT is
a very real concern. Steps must be taken to prevent or minimise the associated
risk. Unfortunately, I have no experience with this sort of thing, so I could
be making some incorrect assumptions below.
One possible way to make obtaining the SSH key for use outside of LOOT
non-trivial is to compile it into the executable. The key could be stored in a
header file, eg. "key.h", of which a template with an empty or dummy key is
committed to the source code repository, and then this header could be included
in the file(s) that require it. So long as the actual key is not distributed
beyond the team member who builds releases, this would then require people to
either reverse-engineer the executable or monitor its allocated memory to obtain
the key.
However, this doesn't prevent the swapping out of files that LOOT uploads. It
may be possible for an attacker to replace the file that LOOT generates or edits
for upload with another containing rubbish data or malicious code. Attackers
could also "cut and paste" the section of the executable that deals with the
uploading into another executable that allows arbitrary uploads. I don't
actually know if the latter is possible, but it sounds like it could be.
In that case, may we lose any upload safeguards, but the attacker could not
disable the download safeguards present in other users' copies of the LOOT
executable unless their systems were already compromised. We have to then assume
that the database is not necessarily safe. Given an unsafe database, we can do
the following:
* Ensure that the robot only has repository commit/checkout permissions and
no admin abilities. Team members should be given repository access to
allow oversight, with admins having admin abilities.
* Check for the validity of database files. A database file should only ever
be read or generated by LOOT, so invalid database files should never
exist except on a compromised system. Any invalid files found could
cause a message to be displayed and a notification sent to the team using
the feedback mechanism. The invalid file would then be deleted. The parser
should fail immediately upon encountering invalid content.
* Don't execute database file content. The format doesn't have any
executable data, so this is pretty obvious in this case.
* Revoke access to the repository for the compromised key. If we only have
one valid robot key at any one time, this is pretty easy. The downside is
that it means that nobody can access the repository with the key,
effectively disabling the online cache. The procedure in case this was
done would be to then figure out how the compromise happened, then try to
stop it happening again, before releasing an update to LOOT with a new
key.
* Only download a database file if a plugin's data isn't found in the local
cache. This means that clients will only download database files for
plugins which are installed and which don't already have cached data. So
if A.esp and B.esp have database file updates available, but the specific
version of A.esp that the user has installed already has its data cached
locally, then only B.esp.lmdb will be updated.
* Users could disable online database access in their copies of LOOT. If
a user thinks that they have downloaded rubbish data, they can delete
their local cache and LOOT will regenerate it the next time it runs.
The validity and non-execute points would not prevent an attacker from
distributing rubbish data, but it would only take one user noticing and
informing the team for this to be caught and dealt with.
Since the database repository would be separate from the masterlist repository,
and the robot wouldn't have access to the latter, users can easily be informed
of any compromise via masterlist messages.
An attacker may also upload files that may never be downloaded by users, but
which are still malicious or illegal in nature. As such, we can't rely on
users vetting the files they download as an indicator for a compromise. One
possibility is for the database to be monitored by having an email notification
system in place that would notify team members of any file uploads and their
content. That would allow us to detect database compromise without any user
involvement, though it does require team activity.
All the above points are suggested on the assumption that we can't have a
dedicated machine to filter out malicious use. Such a machine would be likely
flawed anyway, so the above points are still worth considering even if we had
a dedicated machine.
Install Structure
=================
+1 -1
View File
@@ -17,4 +17,4 @@ sGame: auto # auto, oblivion, skyrim, fallout3, falloutnv
sLastGame: auto # auto, oblivion, skyrim, fallout3, falloutnv
iDebugVerbosity: 0 # 0, 1, 2, 3. Logging takes place if > 0.
bDoTrialRun: false
sLanguage: eng # An ISO 639-3 language string.
sLanguage: eng # An ISO 639-3 language string.
+2 -1
View File
@@ -25,6 +25,7 @@
#define __BOSS_ERROR__
#include <exception>
#include <string>
namespace boss {
@@ -41,7 +42,7 @@ namespace boss {
class error : public std::exception {
public:
error(const int code_arg, const std::string& what_arg) : _code(code_arg), _what(what_arg) {}
error(const unsigned int code_arg, const std::string& what_arg) : _code(code_arg), _what(what_arg) {}
~error() throw() {};
unsigned int code() const { return _code; }
-17
View File
@@ -164,21 +164,4 @@ namespace boss {
throw error(ERROR_PATH_WRITE_FAIL, "Could not create BOSS folder for game.");
}
}
//Can be used to get the location of the LOCALAPPDATA folder (and its Windows XP equivalent).
fs::path Game::GetLocalAppDataPath() {
#if _WIN32 || _WIN64
HWND owner;
TCHAR path[MAX_PATH];
HRESULT res = SHGetFolderPath(owner, CSIDL_LOCAL_APPDATA, NULL, SHGFP_TYPE_CURRENT, path);
if (res == S_OK)
return fs::path(path);
else
return fs::path("");
#else
return fs::path("");
#endif
}
}
-3
View File
@@ -68,9 +68,6 @@ namespace boss {
boost::filesystem::path gamePath; //Path to the game's folder.
boost::unordered_set<std::string> activePlugins; //Holds lowercased strings.
//Can be used to get the location of the LOCALAPPDATA folder (and its Windows XP equivalent).
boost::filesystem::path GetLocalAppDataPath();
};
}
+4
View File
@@ -34,6 +34,10 @@ namespace boss {
const unsigned int VERSION_MAJOR = 3;
const unsigned int VERSION_MINOR = 0;
const unsigned int VERSION_PATCH = 0;
// Temporary hack.
const char * const libespm_options_path = "../../libespm/opts/input";
const char * const libespm_game = "Skyrim";
}
+38 -13
View File
@@ -26,7 +26,7 @@
*/
#include "helpers.h"
#include "plugin/ModFormat.h"
#include "VersionRegex.h"
#include "error.h"
#include <boost/spirit/include/karma.hpp>
@@ -46,6 +46,10 @@
#include <sys/types.h>
#include <sstream>
// Libespm files.
#include <src/commonSupport.h>
#include <src/fileFormat.h>
#if _WIN32 || _WIN64
# ifndef UNICODE
# define UNICODE
@@ -163,10 +167,37 @@ namespace boss {
: verString(ver) {}
Version::Version(const fs::path file) {
// LOG_TRACE("extracting version from '%s'", file.string().c_str());
if (file.extension().string() == ".esm" || file.extension().string() == ".esp") {
//Use libespm interface.
verString = ReadHeader(file).Version;
//Use libespm interface. Assumes that it has previously been initialised.
ifstream input(file.string().c_str(), ios::binary);
espm::file FileStruct;
espm::readFile(input, FileStruct);
input.close();
for(size_t i=0,max=FileStruct.fields.size(); i < max; ++i){
if (FileStruct.fields[i].name == "SNAM") {
string text = FileStruct.fields[i].data;
string::const_iterator begin, end;
begin = text.begin();
end = text.end();
for(int i = 0; regex* re = version_checks[i]; i++) {
smatch what;
while (regex_search(begin, end, what, *re)) {
if (what.empty())
continue;
ssub_match match = what[1];
if (!match.matched)
continue;
verString = trim_copy(string(match.first, match.second));
}
}
}
}
} else {
#if _WIN32 || _WIN64
DWORD dummy = 0;
@@ -193,10 +224,8 @@ namespace boss {
}
#else
// ensure filename has no quote characters in it to avoid command injection attacks
if (string::npos == file.string().find('"')) {
// LOG_WARN("filename has embedded quotes; skipping to avoid command injection: '%s'", file.string().c_str());
// } else {
// command mostly borrowed from the gnome-exe-thumbnailer.sh script
if (string::npos != file.string().find('"')) {
// command mostly borrowed from the gnome-exe-thumbnailer.sh script
// wrestool is part of the icoutils package
string cmd = "wrestool --extract --raw --type=version \"" + file.string() + "\" | tr '\\0, ' '\\t.\\0' | sed 's/\\t\\t/_/g' | tr -c -d '[:print:]' | sed -r 's/.*Version[^0-9]*([0-9]+(\\.[0-9]+)+).*/\\1/'";
@@ -206,11 +235,7 @@ namespace boss {
static const uint32_t BUFSIZE = 32;
char buf[BUFSIZE];
if (NULL != fgets(buf, BUFSIZE, fp)) {
/* LOG_DEBUG("failed to extract version from '%s'", file.string().c_str());
}
else {
*/ verString = string(buf);
// LOG_DEBUG("extracted version from '%s': %s", file.string().c_str(), retVal.c_str());
verString = string(buf);
}
pclose(fp);
}
-212
View File
@@ -1,212 +0,0 @@
#! /usr/bin/env python3
# BOSS
#
# A plugin load order optimiser for games that use the esp/esm plugin system.
#
# Copyright (C) 2012 WrinklyNinja
#
# This file is part of BOSS.
#
# BOSS is free software: you can redistribute
# it and/or modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation, either version 3 of
# the License, or (at your option) any later version.
#
# BOSS is distributed in the hope that it will
# be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BOSS. If not, see
# <http://www.gnu.org/licenses/>.
# This is a script that converts a MF 2.3 masterlist to a MF 3 masterlist. It's
# a bit hacky, but the YAML parser accepts it. Limitations are:
# - Doesn't strip plugins that no longer belong in the masterlist, ie. without
# messages or positioning comments.
# - Requirements and incompatibilities do not use their new data structures,
# and are just converted to 'say' messages.
# - No extraneous comments/messages are removed.
# - VAR conditions are not replaced with their conditional statements. SET lines
# are stripped, so the old masterlist must be used to check what they are.
# - Each message or list of tags comes under a different 'msg' or 'tag' object.
# - Tag message text, aside from the tags themselves, is lost.
# - Conditional plugin positions are all made unconditional, so there will be
# duplicate entries.
class Plugin:
def escapeYAMLStr(s):
special = ['-', '?', ':', ',', '[', ']', '{', '}', '&', '*', '!', '|', '>', '\'', '"', '%', '@', '`']
if ('\'' in s):
return '"' + s.replace('\\', '\\\\').replace('"', '\\"').strip() + '"'
elif (1 in [c in s for c in special]):
return '\'' + s.strip() + '\''
else:
return s;
def mContent(line):
content = line[line.find(':')+1:]
return escapeYAMLStr(content)
def convertCondition(condition):
#IF no longer exists.
#IFNOT is now 'not'.
#The VAR condition no longer exists.
#The '=' comparator is now '=='.
#The LANG condition no longer exists.
#'&&' is now 'and'.
#'||' is now 'or'.
#Really, these will probably have to be done manually.
parts = condition.split('"') # Every odd index is quoted and shouldn't be lowercased.
for i in range(len(parts)):
if (i % 2 == 0):
parts[i] = parts[i].lower()
condition = '"'.join(parts)
condition.replace(' && ', ' and ').replace(' || ', ' or ').replace(' =)', ' ==)')
return escapeYAMLStr(condition)
inFile = open('../../BOSS/data/boss-oblivion/masterlist.txt', 'r')
outFile = open('../build/masterlist.yaml', 'w')
outFile.write('---\n');
isInComment = False
lastLine = ''
lastLineType = 0
for line in inFile:
line = line.replace('\n', '')
if (len(line.strip()) == 0):
continue
# Skip group lines.
if ("BEGINGROUP:" in line or "ENDGROUP:" in line):
continue
# Skip var lines.
if ('SET:' in line):
continue
# Write comments.
if (line[0:2] == '//'):
outFile.write('#' + line[2:] + '\n')
continue
if ('/*' in line):
isInComment = True
outFile.write('#' + line + '\n')
if ('*/' in line):
isInComment = False
outFile.write('#' + line + '\n')
if ('*/' in line or '/*' in line):
continue
if (isInComment):
outFile.write('#' + line + '\n')
continue
# Write plugin lines.
if (':' not in line):
outFile.write(' - name: ' + escapeYAMLStr(line) + '\n')
else:
pos = line.find(':')
if ('MOD:' in line[:pos+1]):
outFile.write(' - name: ' + escapeYAMLStr(line[pos+1:]) + '\n')
elif ('REGEX:' in line[:pos+1]):
outFile.write(' - name: ' + escapeYAMLStr(line[pos+1:]) + '\n')
# Extract conditional from line.
if ('IF' in line and ':' in line):
key = line[:line.find(':')]
key = key.replace('GLOBAL', '')
condition = key[:key.rfind(' ')].strip()
elif ('ELSE' in line and ':' in line):
if (condition[0] != 'n'):
condition = 'not ' + condition
condition = condition.replace('not ', '`~||~`')
condition = condition.replace('d f', 'd not f').replace('d c', 'd not c', ', 'and
else:
condition = ''
condition = convertCondition(condition);
# Write tag lines.
if ('TAG:' in line):
if ('{{BASH' in line):
tags = line[line.find('{{BASH:') + 7:line.find('}}') ]
tags = tags.split(',')
outFile.write(' tag:\n')
for tag in tags:
if (condition):
outFile.write(' - condition: ' + condition + '\n')
outFile.write(' name: ' + tag.strip(' ') + '\n')
else:
outFile.write(' - ' + tag.strip(' ') + '\n')
if ('[' in line):
tags = line[line.find('[')+1:line.find(']')]
tags = tags.split(',')
outFile.write(' tag:\n')
for tag in tags:
if (condition):
outFile.write(' - condition: ' + condition + '\n')
outFile.write(' name: -' + tag.strip(' ') + '\n')
else:
outFile.write(' - -' + tag.strip(' ') + '\n')
# Write 'say' messages.
if ('SAY:' in line or 'INC:' in line or 'REQ:' in line):
if ('GLOBAL ' not in line):
outFile.write(' msg:\n')
indent = ' '
else:
indent = ' '
prefix = ''
if ('INC:' in line):
line = 'Incompatible with ' + line.split(':')[1]
elif ('REQ:' in line):
line = 'Requires ' + line.split(':')[1]
else:
line = line.split(':')[1]
outFile.write(indent + '- type: say\n')
if (condition):
outFile.write(indent + ' condition: ' + condition + '\n')
outFile.write(indent + ' content: ' + escapeYAMLStr(line) + '\n')
# Write 'warn' messages.
if ('DIRTY:' in line or 'WARN:' in line):
if ('GLOBAL ' not in line):
outFile.write(' msg:\n')
indent = ' '
else:
indent = ' '
outFile.write(indent + '- type: warn\n')
if (condition):
outFile.write(indent + ' condition: ' + condition + '\n')
outFile.write(indent + ' content: ' + mContent(line) + '\n')
# Write 'error' messages.
if ('ERROR:' in line):
if ('GLOBAL ' not in line):
outFile.write(' msg:\n')
indent = ' '
else:
indent = ' '
outFile.write(indent + '- type: error\n')
if (condition):
outFile.write(indent + ' condition: ' + condition + '\n')
outFile.write(indent + ' content: ' + mContent(line) + '\n')
inFile.close();
outFile.close();
+58 -2
View File
@@ -22,7 +22,11 @@
*/
#include "metadata.h"
#include "helpers.h"
#include <boost/algorithm/string.hpp>
#include <src/fileFormat.h>
#include <iostream>
using namespace std;
@@ -126,8 +130,31 @@ namespace boss {
return Data();
}
Plugin::Plugin() : enabled(true), priority(0) {}
Plugin::Plugin(const std::string n) : name(n), enabled(true), priority(0) {}
Plugin::Plugin() : enabled(true), priority(0), crc(0), isMaster(false) {}
Plugin::Plugin(const std::string& n) : name(n), enabled(true), priority(0), crc(0), isMaster(false) {}
Plugin::Plugin(const std::string& n, const std::string& path)
: name(n), enabled(true), priority(0) {
// Get data from file contents using libespm. Assumes libespm has already been initialised.
boost::filesystem::path filepath = boost::filesystem::path(path) / n;
ifstream input(filepath.string().c_str(), ios::binary);
espm::file File;
espm::readFile(input, File);
input.close();
crc = GetCrc32(filepath);
isMaster = espm::isMaster(File);
vector<char*> rawMasters = espm::getMasters(File);
for (size_t i=0,max=rawMasters.size(); i < max; ++i) {
masters.push_back(rawMasters[i]);
}
vector<espm::item> records = espm::getRecords(File);
for (vector<espm::item>::const_iterator it = records.begin(),endIt = records.end(); it != endIt; ++it){
formIDs.insert(*reinterpret_cast<uint32_t*>(it->record.recID));
}
}
std::string Plugin::Name() const {
return name;
@@ -241,4 +268,33 @@ namespace boss {
bool Plugin::operator == (Plugin rhs) {
return name == rhs.Name();
}
std::set<uint32_t> Plugin::FormIDs() const {
return formIDs;
}
std::set<uint32_t> Plugin::OverrideFormIDs() const {
int modIndex = masters.size() << 24;
set<uint32_t> fidSubset;
for (set<uint32_t>::const_iterator it = formIDs.begin(), endIt=formIDs.end(); it != endIt; ++it) {
if (*it < modIndex)
fidSubset.insert(*it);
}
return fidSubset;
}
std::vector<std::string> Plugin::Masters() const {
return masters;
}
uint32_t Plugin::Crc() const {
return crc;
}
bool Plugin::IsMaster() const {
return isMaster;
}
}
+38 -2
View File
@@ -25,7 +25,9 @@
#include "game.h"
#include <stdint.h>
#include <string>
#include <vector>
#include <list>
#include <set>
@@ -107,7 +109,8 @@ namespace boss {
class Plugin {
public:
Plugin();
Plugin(const std::string name);
Plugin(const std::string& name);
Plugin(const std::string& name, const std::string& path);
std::string Name() const;
bool Enabled() const;
@@ -132,6 +135,12 @@ namespace boss {
bool IsRegexPlugin() const;
bool operator == (Plugin rhs);
std::set<uint32_t> FormIDs() const;
std::set<uint32_t> OverrideFormIDs() const;
std::vector<std::string> Masters() const;
uint32_t Crc() const;
bool IsMaster() const;
private:
std::string name;
bool enabled; //Default to true.
@@ -141,11 +150,38 @@ namespace boss {
std::set<File, file_comp> incompatibilities;
std::list<Message> messages;
std::list<Tag> tags;
std::vector<std::string> masters;
std::set<uint32_t> formIDs;
uint32_t crc;
bool isMaster;
};
inline bool isMasterOf(const Plugin& servant, const Plugin& master) {
std::vector<std::string> masters = servant.Masters();
for (int i = 0; i < masters.size(); ++i) {
if (masters[i] == master.Name())
return true;
}
return false;
}
struct plugin_comp {
bool operator() (const Plugin& lhs, const Plugin& rhs) const {
return lhs.Name() < rhs.Name();
if (lhs.IsMaster() && !rhs.IsMaster())
return true;
else if (!lhs.IsMaster() && rhs.IsMaster())
return false;
else if (isMasterOf(rhs, lhs))
return true;
else if (isMasterOf(lhs, rhs)) // Should probably also check for cyclic masters.
return false;
else if (lhs.OverrideFormIDs().size() > rhs.OverrideFormIDs().size())
return true;
else if (lhs.OverrideFormIDs().size() < rhs.OverrideFormIDs().size())
return false;
else
return lhs.Name() < rhs.Name();
}
};
+9 -9
View File
@@ -213,14 +213,14 @@ namespace YAML {
Emitter& operator << (Emitter& out, const boss::Message& rhs) {
out << BeginMap
<< Key << "type" << rhs.Type()
<< Key << "content" << rhs.Content();
<< Key << "type" << Value << rhs.Type()
<< Key << "content" << Value << rhs.Content();
if (!rhs.Language().empty())
out << Key << "lang" << rhs.Language();
out << Key << "lang" << Value << rhs.Language();
if (!rhs.Condition().empty())
out << Key << "condition" << rhs.Condition();
out << Key << "condition" << Value << rhs.Condition();
out << EndMap;
}
@@ -230,13 +230,13 @@ namespace YAML {
out << rhs.Name();
else {
out << BeginMap
<< Key << "name" << rhs.Name();
<< Key << "name" << Value << rhs.Name();
if (rhs.IsConditional())
out << Key << "condition" << rhs.Condition();
out << Key << "condition" << Value << rhs.Condition();
if (!rhs.DisplayName().empty())
out << Key << "display" << rhs.DisplayName();
out << Key << "display" << Value << rhs.DisplayName();
out << EndMap;
}
@@ -247,8 +247,8 @@ namespace YAML {
out << rhs.PrefixedName();
else {
out << BeginMap
<< Key << "name" << rhs.PrefixedName()
<< Key << "condition" << rhs.Condition()
<< Key << "name" << Value << rhs.PrefixedName()
<< Key << "condition" << Value << rhs.Condition()
<< EndMap;
}
}
-240
View File
@@ -1,240 +0,0 @@
/* BOSS
A "one-click" program for users that quickly optimises and avoids
detrimental conflicts in their TES IV: Oblivion, Nehrim - At Fate's Edge,
TES V: Skyrim, Fallout 3 and Fallout: New Vegas mod load orders.
Copyright (C) 2009-2012 BOSS Development Team.
This file is part of BOSS.
BOSS is free software: you can redistribute
it and/or modify it under the terms of the GNU General Public License
as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
BOSS is distributed in the hope that it will
be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with BOSS. If not, see
<http://www.gnu.org/licenses/>.
$Revision: 2488 $, $Date: 2011-03-27 14:31:33 +0100 (Sun, 27 Mar 2011) $
*/
#include "Types.h"
#include "ModFormat.h"
#include "VersionRegex.h"
#include <cstring>
#include <fstream>
#include <boost/regex.hpp>
#include <boost/algorithm/string/trim.hpp>
namespace boss {
using namespace std;
using boost::algorithm::trim_copy;
using boost::regex;
//
// string ParseVersion(string&):
// - Tries to extract the version string value from the given text,
// using the above defined regexes to do the dirty work.
//
string ParseVersion(const string& text){
string::const_iterator begin, end;
begin = text.begin();
end = text.end();
for(int i = 0; regex* re = version_checks[i]; i++) {
smatch what;
while (regex_search(begin, end, what, *re)) {
if (what.empty()){
continue;
}
ssub_match match = what[1];
if (!match.matched) {
continue;
}
return trim_copy(string(match.first, match.second));
}
}
return string();
}
//
// string ReadString(pointer&, maxsize):
// - Reads a consecutive array of charactes up to maxsize length and
// returns them as a new string.
//
string ReadString(char*& bufptr, ushort size){
string data;
data.reserve(size + 1);
while (char c = *bufptr++) {
data.append(1, c);
}
return data;
}
//
// T Peek<T>(pointer&):
// - Peeks into the received buffer and returns the value pointed
// converting it to the type T.
//
template <typename T>
T Peek(char* buffer) {
return *reinterpret_cast<T*>(buffer);
}
//
// T Read<T>(pointer&):
// - Tries to extract a value of the specified type T from the
// received buffer, incrementing the pointer to point past the readen
// value.
//
template <typename T>
inline T Read(char*& buffer) {
T value = Peek<T>(buffer);
buffer += sizeof(T);
return value;
}
//-
// ModHeader ReadHeader(string):
// - Parses the mod file contents and extract the header information
// returning the most important data using a ModHeader struct.
// --> see:
// http://www.uesp.net/wiki/Tes4Mod:Mod_File_Format,
//
// and in particular this link:
// http://www.uesp.net/wiki/Tes4Mod:Mod_File_Format/TES4
//
bool IsPluginMaster(boost::filesystem::path filename) {
char buffer[MAXLENGTH];
char* bufptr = buffer;
ModHeader modHeader;
if (filename.empty())
return false;
ifstream file(filename.string().c_str(), ios_base::binary | ios_base::in);
if (file.bad())
return false;
// Reads the first MAXLENGTH bytes into the buffer
file.read(&buffer[0], sizeof(buffer));
// Check for the 'magic' marker at start
if (Read<uint>(bufptr) != Record::TES4){
return false;
}
// Next field is the total header size
/*uint headerSize =*/ Read<uint>(bufptr);
// Next comes the header record Flags
uint flags = Read<uint>(bufptr);
// LSb of this record's flags is used to indicate if the
// mod is a master or a plugin
return ((flags & 0x1) != 0);
}
ModHeader ReadHeader(boost::filesystem::path filename) {
char buffer[MAXLENGTH];
char* bufptr = buffer;
ModHeader modHeader;
ifstream file(filename.string().c_str(), ios_base::binary | ios_base::in);
modHeader.Name = filename.string();
// Reads the first MAXLENGTH bytes into the buffer
file.read(&buffer[0], sizeof(buffer));
// Check for the 'magic' marker at start
if (Read<uint>(bufptr) != Record::TES4){
return modHeader;
}
// Next field is the total header size
/*uint headerSize =*/ Read<uint>(bufptr);
// Next comes the header record Flags
uint flags = Read<uint>(bufptr);
// LSb of this record's flags is used to indicate if the
// mod is a master or a plugin
modHeader.IsMaster = (flags & 0x1) != 0;
// Next comes the FormID...
/*uint formId =*/ Read<uint>(bufptr); // skip formID
// ...and extra flags
/*uint flags2 =*/ Read<uint>(bufptr); // skip flags2
// Here the Header record starts, check for its signature 'HEDR'
if (Read<uint>(bufptr) != Record::HEDR){
return modHeader;
}
// HEDR record has fields: DataSize, Version (0.8 o 1.0), Record Count
// and Next Object Id
/*ushort dataSize =*/ Read<ushort>(bufptr);
/*float version =*/ Read<float>(bufptr);
/*int numRecords =*/ Read<int>(bufptr);
/*uint nextObjId =*/ Read<uint>(bufptr);
// Then comes the sub-records
uint signature = Read<uint>(bufptr);
// skip optional records
bool loop = true;
while (loop){
switch (signature)
{
case Record::OFST:
case Record::DELE:
bufptr += Read<ushort>(bufptr); // skip
signature = Read<uint>(bufptr);
break;
// extract author name, if present
case Record::CNAM:
modHeader.Author = ReadString(bufptr, Read<ushort>(bufptr));
signature = Read<uint>(bufptr);
break;
// extract description and version, if present
case Record::SNAM:
modHeader.Description = ReadString(bufptr, Read<ushort>(bufptr));
modHeader.Version = ParseVersion(modHeader.Description);
signature = Read<uint>(bufptr);
break;
default:
loop = false;
}
}
// We should have all the required information.
return modHeader;
}
}
-64
View File
@@ -1,64 +0,0 @@
/* BOSS
A "one-click" program for users that quickly optimises and avoids
detrimental conflicts in their TES IV: Oblivion, Nehrim - At Fate's Edge,
TES V: Skyrim, Fallout 3 and Fallout: New Vegas mod load orders.
Copyright (C) 2009-2012 BOSS Development Team.
This file is part of BOSS.
BOSS is free software: you can redistribute
it and/or modify it under the terms of the GNU General Public License
as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
BOSS is distributed in the hope that it will
be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with BOSS. If not, see
<http://www.gnu.org/licenses/>.
$Revision: 2488 $, $Date: 2011-03-27 14:31:33 +0100 (Sun, 27 Mar 2011) $
*/
#ifndef __SUPPORT_MODFORMAT__HPP__
#define __SUPPORT_MODFORMAT__HPP__
#include "Types.h"
#include <cstring>
#include <boost/filesystem.hpp>
namespace boss {
using namespace std;
// Structure for grouping the information gathered from each mod's header.
struct ModHeader {
string Name;
string Description;
string Author;
string Version;
bool IsMaster;
};
struct Record {
static const ulong TES4 = '4SET';
static const ulong HEDR = 'RDEH';
static const ulong OFST = 'TSFO';
static const ulong DELE = 'ELED';
static const ulong CNAM = 'MANC';
static const ulong SNAM = 'MANS';
};
ModHeader ReadHeader(boost::filesystem::path filename);
bool IsPluginMaster(boost::filesystem::path filename); //Shorter version of the above, to only get master flag.
}
#endif
-51
View File
@@ -1,51 +0,0 @@
/* BOSS
A "one-click" program for users that quickly optimises and avoids
detrimental conflicts in their TES IV: Oblivion, Nehrim - At Fate's Edge,
TES V: Skyrim, Fallout 3 and Fallout: New Vegas mod load orders.
Copyright (C) 2009-2012 BOSS Development Team.
This file is part of BOSS.
BOSS is free software: you can redistribute
it and/or modify it under the terms of the GNU General Public License
as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
BOSS is distributed in the hope that it will
be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with BOSS. If not, see
<http://www.gnu.org/licenses/>.
$Revision: 1679 $, $Date: 2010-10-17 20:41:49 +0100 (Sun, 17 Oct 2010) $
*/
#ifndef __SUPPORT_TYPES__HPP__
#define __SUPPORT_TYPES__HPP__
namespace boss {
//////////////////////////////////////////////////////////////////////////
// Common type definitions
//////////////////////////////////////////////////////////////////////////
typedef unsigned long ulong;
typedef unsigned int uint;
typedef unsigned short ushort;
//////////////////////////////////////////////////////////////////////////
// Constants
//////////////////////////////////////////////////////////////////////////
// An arbitrary large number. Controls the size of some buffers used to read data from files.
const ulong MAXLENGTH = 4096UL;
}
#endif
-92
View File
@@ -1,92 +0,0 @@
/* BOSS
A "one-click" program for users that quickly optimises and avoids
detrimental conflicts in their TES IV: Oblivion, Nehrim - At Fate's Edge,
TES V: Skyrim, Fallout 3 and Fallout: New Vegas mod load orders.
Copyright (C) 2009-2012 BOSS Development Team.
This file is part of BOSS.
BOSS is free software: you can redistribute
it and/or modify it under the terms of the GNU General Public License
as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
BOSS is distributed in the hope that it will
be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with BOSS. If not, see
<http://www.gnu.org/licenses/>.
$Revision: 1681 $, $Date: 2010-10-17 21:01:25 +0100 (Sun, 17 Oct 2010) $
*/
#include <string>
#include <boost/regex.hpp>
namespace boss {
using namespace std;
using namespace boost;
/// REGEX expression definition
/// Each expression is composed of three parts:
/// 1. The marker string "version", "ver", "rev", "v" or "r"
/// 2. The version string itself.
const char* regex1 =
"^(?:\\bversion\\b[ ]*(?:[:.\\-]?)|\\brevision\\b(?:[:.\\-]?))[ ]*"
"((?:alpha|beta|test|debug)?\\s*[-0-9a-zA-Z._+]+\\s*(?:alpha|beta|test|debug)?\\s*(?:[0-9]*))$"
;
const char* regex2 =
"(?:\\bversion\\b(?:[ :]?)|\\brevision\\b(?:[:.\\-]?))[ ]*"
"([0-9][-0-9a-zA-Z._]+\\+?)"
;
const char* regex3 =
"(?:\\bver(?:[:.]?)|\\brev(?:[:.]?))\\s*"
"([0-9][-0-9a-zA-Z._]*\\+?)"
;
// Matches "Updated: <date>" for the Bashed patch
const char* regex4 =
"(?:Updated:)\\s*"
"([-0-9aAmMpP/ :]+)$"
;
// Matches isolated versions as last resort
const char* regex5 =
"(?:(?:\\bv|\\br)(?:\\s?)(?:[-.:])?(?:\\s*))"
"((?:(?:\\balpha\\b)?|(?:\\bbeta\\b)?)\\s*[0-9][-0-9a-zA-Z._]*\\+?)"
;
// Matches isolated versions as last resort
const char* regex6 =
"((?:(?:\\balpha\\b)?|(?:\\bbeta\\b)?)\\s*\\b[0-9][-0-9a-zA-Z._]*\\+?)$"
;
const char* regex7 =
"(^\\bmark\\b\\s*\\b[IVX0-9][-0-9a-zA-Z._+]*\\s*(?:alpha|beta|test|debug)?\\s*(?:[0-9]*)?)$"
;
/// Array used to try each of the expressions defined above using
/// an iteration for each of them.
regex* version_checks[] = {
new regex(regex1, regex::icase),
new regex(regex2, regex::icase),
new regex(regex3, regex::icase),
new regex(regex4, regex::icase),
new regex(regex5, regex::icase), //This incorrectly identifies "OBSE v19" where 19 is any integer.
new regex(regex6, regex::icase), //This is responsible for metallicow's false positive.
new regex(regex7, regex::icase),
0
};
}
-45
View File
@@ -1,45 +0,0 @@
/* BOSS
A "one-click" program for users that quickly optimises and avoids
detrimental conflicts in their TES IV: Oblivion, Nehrim - At Fate's Edge,
TES V: Skyrim, Fallout 3 and Fallout: New Vegas mod load orders.
Copyright (C) 2009-2012 BOSS Development Team.
This file is part of BOSS.
BOSS is free software: you can redistribute
it and/or modify it under the terms of the GNU General Public License
as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
BOSS is distributed in the hope that it will
be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with BOSS. If not, see
<http://www.gnu.org/licenses/>.
$Revision: 1681 $, $Date: 2010-10-17 21:01:25 +0100 (Sun, 17 Oct 2010) $
*/
#ifndef __SUPPORT_VERSIONREGEX__HPP__
#define __SUPPORT_VERSIONREGEX__HPP__
#include <boost/regex.hpp>
namespace boss {
using namespace boost;
/// Array used to try each of the expressions defined above using
/// an iteration for each of them.
extern regex* version_checks[];
}
#endif
-212
View File
@@ -1,212 +0,0 @@
/* BOSS
A plugin load order optimiser for games that use the esp/esm plugin system.
Copyright (C) 2012 WrinklyNinja
This file is part of BOSS.
BOSS is free software: you can redistribute
it and/or modify it under the terms of the GNU General Public License
as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
BOSS is distributed in the hope that it will
be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with BOSS. If not, see
<http://www.gnu.org/licenses/>.
*/
#include "plugindata.h"
#include <vector>
#include <cctype>
#include <algorithm>
#include <fstream>
#include <new>
#include <boost/filesystem.hpp>
namespace boss {
PluginData::PluginData() : crc(0) {}
PluginData::PluginData(std::string filename) : name(filename) {
//Load by scanning plugin file using libespm.
}
PluginData::PluginData(std::string filename, uint32_t fileCRC) : name(filename), crc(fileCRC) {
//Load by scanning cache file.
std::string file = name[0] + '/' + name[1] + '/' + name + ".dc";
//Lowercase the path.
std::for_each(file.begin(), file.end(), tolower);
if (!boost::filesystem::exists(file))
return;
ifstream ifile(file.c_str(), ios_base::binary);
ifile.exceptions(ifstream::failbit | ifstream::badbit | ifstream::eofbit);
//Get the number of entries.
uint32_t count;
ifile.seekg(4, ios_base::end);
ifile.read((char*)&count, 4);
//Read index.
CacheIndexEntry * index;
try {
index = new CacheIndexEntry[count];
} catch (std::bad_alloc& e) {
return;
}
ifile.seekg(4 + count * sizeof(CacheIndexEntry), ios_base::end);
ifile.read((char*)index, count * sizeof(CacheIndexEntry));
//Search index.
uint32_t i = 0;
for (i; i < count; i++) {
if (index[i].crc == crc)
break;
}
if (i == count)
return;
//Read plugin data size.
uint32_t dataSize;
ifile.seekg(index[i].offset, ios_base::beg);
ifile.read((char*)&dataSize, 4);
//Read plugin data.
char * data;
try {
data = new char[dataSize];
} catch (std::bad_alloc& e) {
return;
}
ifile.read(data, dataSize);
//Now we need to read the list of masters.
uint16_t mastersLen = *(uint16_t*)data;
std::string master;
std::vector<std::string> masters;
for (int i=2; i < mastersLen; i++) {
if (data[i] != NULL)
master += data[i];
else {
masters.push_back(master);
master.clear();
}
}
//Now read the FormIDs, building up FormID objects from them and their
//associated masters.
for (int i=2+mastersLen; i < dataSize; i += 4) {
int masterID = data[i+3]; //FormIDs are encoded low to high byte.
FormID fid;
fid.plugin = masters[masterID];
fid.objectIndex = *(uint32_t*)data & 0xFFFFFF; //Zero the masterID.
formIDs.insert(fid);
}
ifile.close();
}
PluginData::Save() const {
//Save to a cache file.
std::string outPath = name[0] + '/' + name[1] + '/' + name + ".dc";
//Lowercase the path.
std::for_each(outPath.begin(), outPath.end(), tolower);
uint32_t count = 0;
CacheIndexEntry * index = NULL;
if (boost::filesystem::exists(file)) {
ifstream ifile(outPath.c_str(), ios_base::binary);
ifile.exceptions(ifstream::failbit | ifstream::badbit | ifstream::eofbit);
//Get the number of entries.
ifile.seekg(4, ios_base::end);
ifile.read((char*)&count, 4);
//Read index.
try {
index = new CacheIndexEntry[count];
} catch (std::bad_alloc& e) {
return;
}
ifile.seekg(4 + count * sizeof(CacheIndexEntry), ios_base::end);
ifile.read((char*)&index, count * sizeof(CacheIndexEntry));
ifile.close();
}
/*Split the FormID object list into a FormID number list and a master list.
This doesn't retain the same ordering of masters as the original data,
but that doesn't matter. */
std::map<std::string, int> masters;
std::list<uint32_t> fids;
std::string mastersList;
int id = 0;
for (std::list<FormID>::iterator it = formIDs.begin(), endIt = formIDs.end(); it != endIt; ++it) {
std::map<std::string, int>::iterator mit = masters.find(it->plugin);
if (mit != masters.end())
fids.insert(it->objectIndex | (mit->second<<24));
else {
id++;
masters.insert(pair<std::string, int>(it->plugin, id);
fids.insert(it->objectIndex | (id<<24));
mastersList += it->plugin + '\0';
}
}
ofstream ofile(file.c_str(), ios_base::binary);
ofile.exceptions(ifstream::failbit | ifstream::badbit | ifstream::eofbit);
CacheIndexEntry entry;
entry.crc = crc;
if (!boost::filesystem::exists(file)) {
uint8_t nameLength = name.length();
ofile.write((char*)&nameLenth, 1);
ofile.write((char*)name.data(), nameLength);
entry.offset = tellg();
} else {
ofile.seekg(4 + count * sizeof(CacheIndexEntry), ios_base::end);
entry.offset = ofile.tellg();
}
//Write out data.
uint16_t mLen = mastersList.size();
ofile.write((char*)&mLen, 2);
ofile.write(mastersList.data(), mLen);
for (std::list<uint32_t>::iterator it = formIDs.begin(), endIt = formIds.end(); it != endIt; ++it) {
ofile.write((char*)it, 4);
}
//Write out updated index.
if (index != NULL)
ofile.write((char*)index, count * sizeof(CacheIndexEntry));
ofile.write((char*)&entry, sizeof(CacheIndexEntry));
count++;
ofile.write((char*)&count, 4);
ofile.close();
}
PluginData::Empty() const {
return formIDs.empty();
}
int PluginData::Overlap(const PluginData& otherPlugin) const {
int count = 0;
for (std::list<FormID>::iterator it1 = formIDs.begin(), endIt1 = formIDs.end(); it1 != endIt1; ++it1) {
for (std::list<FormID>::iterator it2 = otherPlugin.formIDs.begin(), endIt2 = otherPlugin.formIDs.end(); it2 != endIt2; ++it2) {
if (*it1 == *it2)
count++;
}
}
}
}
-67
View File
@@ -1,67 +0,0 @@
/* BOSS
A plugin load order optimiser for games that use the esp/esm plugin system.
Copyright (C) 2012 WrinklyNinja
This file is part of BOSS.
BOSS is free software: you can redistribute
it and/or modify it under the terms of the GNU General Public License
as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
BOSS is distributed in the hope that it will
be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with BOSS. If not, see
<http://www.gnu.org/licenses/>.
*/
#ifndef __BOSS_PLUGINDATA_H__
#define __BOSS_PLUGINDATA_H__
#include <list>
#include <string>
#include <stdint.h>
//The functions below make no distinctions between different games. Once a game
//handler is implemented, they will be updated to use it.
namespace boss {
struct CacheIndexEntry {
uint32_t crc;
uint32_t offset;
}
struct FormID {
std::string plugin;
uint32_t objectIndex;
}
struct PluginData {
std::string name;
uint32_t crc;
std::list<FormID> formIDs; // Only the edited FormIDs though.
PluginData();
PluginData(std::string pluginPath);
/* Can throw std::bad_alloc or std::ios::failure */
PluginData(std::string name, uint32_t crc);
/* Can throw std::bad_alloc or std::ios::failure */
void Save() const;
bool Empty() const;
// Returns the record overlap of two PluginData objects.
int Overlap(const PluginData& otherPlugin) const;
}
}
#endif

Some files were not shown because too many files have changed in this diff Show More