Files
fluxengine/lib/config.cc

688 lines
17 KiB
C++

#include "lib/globals.h"
#include "lib/config.h"
#include "lib/proto.h"
#include "lib/logger.h"
#include "lib/utils.h"
#include "lib/imagewriter/imagewriter.h"
#include "lib/imagereader/imagereader.h"
#include "lib/fluxsink/fluxsink.h"
#include "lib/fluxsource/fluxsource.h"
#include "lib/encoders/encoders.h"
#include "lib/decoders/decoders.h"
#include <fstream>
#include <google/protobuf/text_format.h>
static Config config;
enum ConstructorMode
{
MODE_RO,
MODE_WO,
MODE_RW
};
struct ImageConstructor
{
std::string extension;
ImageReaderWriterType type;
ConstructorMode mode;
};
static const std::vector<FluxConstructor> fluxConstructors = {
{/* The .flux format must be first. */
.name = "FluxEngine (.flux)",
.pattern = std::regex("^(.*\\.flux)$"),
.source =
[](auto& s, auto* proto)
{
proto->set_type(FLUXTYPE_FLUX);
proto->mutable_fl2()->set_filename(s);
}, .sink =
[](auto& s, auto* proto)
{
proto->set_type(FLUXTYPE_FLUX);
proto->mutable_fl2()->set_filename(s);
}},
{
.name = "Supercard Pro (.scp)",
.pattern = std::regex("^(.*\\.scp)$"),
.source =
[](auto& s, auto* proto)
{
proto->set_type(FLUXTYPE_SCP);
proto->mutable_scp()->set_filename(s);
}, .sink =
[](auto& s, auto* proto)
{
proto->set_type(FLUXTYPE_SCP);
proto->mutable_scp()->set_filename(s);
}, },
{.name = "AppleSauce (.a2r)",
.pattern = std::regex("^(.*\\.a2r)$"),
.source =
[](auto& s, auto* proto)
{
proto->set_type(FLUXTYPE_A2R);
proto->mutable_a2r()->set_filename(s);
}, .sink =
[](auto& s, auto* proto)
{
proto->set_type(FLUXTYPE_A2R);
proto->mutable_a2r()->set_filename(s);
}},
{.name = "CatWeazle (.cwf)",
.pattern = std::regex("^(.*\\.cwf)$"),
.source =
[](auto& s, auto* proto)
{
proto->set_type(FLUXTYPE_CWF);
proto->mutable_cwf()->set_filename(s);
}},
{.pattern = std::regex("^erase:$"),
.source =
[](auto& s, auto* proto)
{
proto->set_type(FLUXTYPE_ERASE);
}},
{.name = "KryoFlux directory",
.pattern = std::regex("^kryoflux:(.*)$"),
.source =
[](auto& s, auto* proto)
{
proto->set_type(FLUXTYPE_KRYOFLUX);
proto->mutable_kryoflux()->set_directory(s);
}},
{.pattern = std::regex("^testpattern:(.*)"),
.source =
[](auto& s, auto* proto)
{
proto->set_type(FLUXTYPE_TEST_PATTERN);
}},
{.pattern = std::regex("^drive:(.*)"),
.source =
[](auto& s, auto* proto)
{
proto->set_type(FLUXTYPE_DRIVE);
globalConfig().overrides()->mutable_drive()->set_drive(
std::stoi(s));
}, .sink =
[](auto& s, auto* proto)
{
proto->set_type(FLUXTYPE_DRIVE);
globalConfig().overrides()->mutable_drive()->set_drive(
std::stoi(s));
}},
{.name = "FluxCopy directory",
.pattern = std::regex("^flx:(.*)$"),
.source =
[](auto& s, auto* proto)
{
proto->set_type(FLUXTYPE_FLX);
proto->mutable_flx()->set_directory(s);
}},
{.name = "Value Change Dump directory",
.pattern = std::regex("^vcd:(.*)$"),
.sink =
[](auto& s, auto* proto)
{
proto->set_type(FLUXTYPE_VCD);
proto->mutable_vcd()->set_directory(s);
}},
{.name = "Audio file directory",
.pattern = std::regex("^au:(.*)$"),
.sink =
[](auto& s, auto* proto)
{
proto->set_type(FLUXTYPE_AU);
proto->mutable_au()->set_directory(s);
}},
};
static const std::vector<ImageConstructor> imageConstructors = {
{".adf", IMAGETYPE_IMG, MODE_RW},
{".d64", IMAGETYPE_D64, MODE_RW},
{".d81", IMAGETYPE_IMG, MODE_RW},
{".d88", IMAGETYPE_D88, MODE_RW},
{".dim", IMAGETYPE_DIM, MODE_RO},
{".diskcopy", IMAGETYPE_DISKCOPY, MODE_RW},
{".dsk", IMAGETYPE_IMG, MODE_RW},
{".fdi", IMAGETYPE_FDI, MODE_RO},
{".imd", IMAGETYPE_IMD, MODE_RW},
{".img", IMAGETYPE_IMG, MODE_RW},
{".jv3", IMAGETYPE_JV3, MODE_RO},
{".nfd", IMAGETYPE_NFD, MODE_RO},
{".nsi", IMAGETYPE_NSI, MODE_RW},
{".st", IMAGETYPE_IMG, MODE_RW},
{".td0", IMAGETYPE_TD0, MODE_RO},
{".vgi", IMAGETYPE_IMG, MODE_RW},
{".xdf", IMAGETYPE_IMG, MODE_RW},
};
Config& globalConfig()
{
return config;
}
ConfigProto* Config::combined()
{
if (!_configValid)
{
_combinedConfig = _baseConfig;
/* First apply any standalone options. */
std::set<std::string> options = _appliedOptions;
for (const auto& option : _baseConfig.option())
{
if (options.find(option.name()) != options.end())
{
_combinedConfig.MergeFrom(option.config());
options.erase(option.name());
}
}
/* Then apply any group options. */
for (auto& group : _baseConfig.option_group())
{
const OptionProto* selectedOption = &*group.option().begin();
for (auto& option : group.option())
{
if (options.find(option.name()) != options.end())
{
selectedOption = &option;
options.erase(option.name());
}
}
_combinedConfig.MergeFrom(selectedOption->config());
}
/* Add in the user overrides. */
_combinedConfig.MergeFrom(_overridesConfig);
/* At this point the config is mostly valid. We're about to make calls
* that will want to call combined() reentrantly, so to prevent infinite
* loop we mark the config as valid now. */
_configValid = true;
/* We should now be more or less done, but we still need to add in any
* config contributed by the flux source and image readers. This will
* open the files. */
if (hasFluxSource())
_combinedConfig.MergeFrom(getFluxSource()->getExtraConfig());
if (hasImageReader())
_combinedConfig.MergeFrom(getImageReader()->getExtraConfig());
/* Merge in the overrides once again. */
_combinedConfig.MergeFrom(_overridesConfig);
}
return &_combinedConfig;
}
void Config::invalidate()
{
_configValid = false;
}
void Config::clear()
{
_configValid = false;
_baseConfig.Clear();
_overridesConfig.Clear();
_combinedConfig.Clear();
_fluxSource.reset();
_verificationFluxSource.reset();
_imageReader.reset();
_encoder.reset();
_decoder.reset();
_appliedOptions.clear();
}
std::vector<std::string> Config::validate()
{
std::vector<std::string> results;
std::set<std::string> optionNames = _appliedOptions;
std::set<const OptionProto*> appliedOptions;
for (const auto& option : _baseConfig.option())
{
if (optionNames.find(option.name()) != optionNames.end())
{
appliedOptions.insert(&option);
optionNames.erase(option.name());
}
}
/* Then apply any group options. */
for (auto& group : _baseConfig.option_group())
{
int count = 0;
for (auto& option : group.option())
{
if (optionNames.find(option.name()) != optionNames.end())
{
optionNames.erase(option.name());
appliedOptions.insert(&option);
count++;
if (count == 2)
results.push_back(
fmt::format("multiple mutually exclusive options set "
"for group '{}'",
group.comment()));
}
}
}
/* Check for unknown options. */
if (!optionNames.empty())
{
for (auto& name : optionNames)
results.push_back(fmt::format("'{}' is not a known option", name));
}
/* Check option requirements. */
for (auto& option : appliedOptions)
{
try
{
checkOptionValid(*option);
}
catch (const InapplicableOptionException& e)
{
results.push_back(e.message);
}
}
return results;
}
void Config::validateAndThrow()
{
auto r = validate();
if (!r.empty())
{
std::stringstream ss;
ss << "invalid configuration:\n";
for (auto& s : r)
ss << s << '\n';
throw InapplicableOptionException(ss.str());
}
}
void Config::set(std::string key, std::string value)
{
setProtoByString(overrides(), key, value);
}
void Config::setTransient(std::string key, std::string value)
{
setProtoByString(&_combinedConfig, key, value);
}
std::string Config::get(std::string key)
{
return getProtoByString(combined(), key);
}
static ConfigProto loadSingleConfigFile(std::string filename)
{
const auto& it = formats.find(filename);
if (it != formats.end())
return *it->second;
else
{
std::ifstream f(filename, std::ios::out);
if (f.fail())
error("Cannot open '{}': {}", filename, strerror(errno));
std::ostringstream ss;
ss << f.rdbuf();
ConfigProto config;
if (!google::protobuf::TextFormat::MergeFromString(ss.str(), &config))
error("couldn't load external config proto");
return config;
}
}
void Config::readBaseConfigFile(std::string filename)
{
base()->MergeFrom(loadSingleConfigFile(filename));
}
void Config::readBaseConfig(std::string data)
{
if (!google::protobuf::TextFormat::MergeFromString(data, base()))
error("couldn't load external config proto");
}
const OptionProto& Config::findOption(const std::string& optionName)
{
const OptionProto* found = nullptr;
auto searchOptionList = [&](auto& optionList)
{
for (const auto& option : optionList)
{
if (optionName == option.name())
{
found = &option;
return true;
}
}
return false;
};
if (searchOptionList(base()->option()))
return *found;
for (const auto& optionGroup : base()->option_group())
{
if (searchOptionList(optionGroup.option()))
return *found;
}
throw OptionNotFoundException(
fmt::format("option {} not found", optionName));
}
void Config::checkOptionValid(const OptionProto& option)
{
for (const auto& req : option.prerequisite())
{
bool matched = false;
try
{
auto value = get(req.key());
for (auto requiredValue : req.value())
matched |= (requiredValue == value);
}
catch (const ProtoPathNotFoundException e)
{
/* This field isn't available, therefore it
* cannot match. */
}
if (!matched)
{
std::stringstream ss;
ss << '[';
bool first = true;
for (auto requiredValue : req.value())
{
if (!first)
ss << ", ";
ss << quote(requiredValue);
first = false;
}
ss << ']';
throw InapplicableOptionException(
fmt::format("option '{}' is inapplicable to this "
"configuration "
"because {}={} could not be met",
option.name(),
req.key(),
ss.str()));
}
}
}
bool Config::isOptionValid(const OptionProto& option)
{
try
{
checkOptionValid(option);
return true;
}
catch (const InapplicableOptionException& e)
{
return false;
}
}
bool Config::isOptionValid(std::string option)
{
return isOptionValid(findOption(option));
}
void Config::applyOption(const OptionProto& option)
{
log("OPTION: {}",
option.has_message() ? option.message() : option.comment());
_appliedOptions.insert(option.name());
}
void Config::applyOption(std::string option)
{
applyOption(findOption(option));
}
void Config::clearOptions()
{
_appliedOptions.clear();
invalidate();
}
static void setFluxSourceImpl(
const std::string& filename, FluxSourceProto* proto)
{
for (const auto& it : fluxConstructors)
{
std::smatch match;
if (std::regex_match(filename, match, it.pattern))
{
if (!it.source)
throw new InapplicableValueException();
it.source(match[1], proto);
return;
}
}
error("unrecognised flux filename '{}'", filename);
}
void Config::setFluxSource(std::string filename)
{
setFluxSourceImpl(filename, overrides()->mutable_flux_source());
}
static void setFluxSinkImpl(const std::string& filename, FluxSinkProto* proto)
{
for (const auto& it : fluxConstructors)
{
std::smatch match;
if (std::regex_match(filename, match, it.pattern))
{
if (!it.sink)
throw new InapplicableValueException();
it.sink(match[1], proto);
return;
}
}
error("unrecognised flux filename '{}'", filename);
}
void Config::setFluxSink(std::string filename)
{
setFluxSinkImpl(filename, overrides()->mutable_flux_sink());
}
void Config::setCopyFluxTo(std::string filename)
{
setFluxSinkImpl(
filename, overrides()->mutable_decoder()->mutable_copy_flux_to());
}
void Config::setVerificationFluxSource(std::string filename)
{
setFluxSourceImpl(filename, &_verificationFluxSourceProto);
}
void Config::setImageReader(std::string filename)
{
for (const auto& it : imageConstructors)
{
if (endsWith(filename, it.extension))
{
if (it.mode == MODE_WO)
throw new InapplicableValueException();
overrides()->mutable_image_reader()->set_type(it.type);
overrides()->mutable_image_reader()->set_filename(filename);
return;
}
}
error("unrecognised image filename '{}'", filename);
}
void Config::setImageWriter(std::string filename)
{
for (const auto& it : imageConstructors)
{
if (endsWith(filename, it.extension))
{
if (it.mode == MODE_RO)
throw new InapplicableValueException();
overrides()->mutable_image_writer()->set_type(it.type);
overrides()->mutable_image_writer()->set_filename(filename);
return;
}
}
error("unrecognised image filename '{}'", filename);
}
bool Config::hasFluxSource()
{
return (*this)->flux_source().type() != FLUXTYPE_NOT_SET;
}
std::shared_ptr<FluxSource>& Config::getFluxSource()
{
if (!_fluxSource)
{
if (!hasFluxSource())
error("no flux source configured");
_fluxSource =
std::shared_ptr(FluxSource::create((*this)->flux_source()));
}
return _fluxSource;
}
bool Config::hasVerificationFluxSource() const
{
return _verificationFluxSourceProto.type() != FLUXTYPE_NOT_SET;
}
std::shared_ptr<FluxSource>& Config::getVerificationFluxSource()
{
if (!_verificationFluxSource)
{
if (!hasVerificationFluxSource())
error("no verification flux source configured");
_verificationFluxSource =
std::shared_ptr(FluxSource::create(_verificationFluxSourceProto));
}
return _verificationFluxSource;
}
bool Config::hasImageReader()
{
return (*this)->image_reader().type() != IMAGETYPE_NOT_SET;
}
std::shared_ptr<ImageReader>& Config::getImageReader()
{
if (!_imageReader)
{
if (!hasImageReader())
error("no image reader configured");
_imageReader =
std::shared_ptr(ImageReader::create((*this)->image_reader()));
}
return _imageReader;
}
bool Config::hasFluxSink()
{
return (*this)->flux_sink().type() != FLUXTYPE_NOT_SET;
}
std::unique_ptr<FluxSink> Config::getFluxSink()
{
if (!hasFluxSink())
error("no flux sink configured");
return FluxSink::create((*this)->flux_sink());
}
bool Config::hasImageWriter()
{
return (*this)->image_writer().type() != IMAGETYPE_NOT_SET;
}
std::unique_ptr<ImageWriter> Config::getImageWriter()
{
if (!hasImageWriter())
error("no image writer configured");
return ImageWriter::create((*this)->image_writer());
}
bool Config::hasEncoder()
{
return (*this)->has_encoder();
}
std::shared_ptr<Encoder>& Config::getEncoder()
{
if (!_encoder)
{
if (!hasEncoder())
error("no encoder configured");
_encoder = Encoder::create((*this)->encoder());
}
return _encoder;
}
bool Config::hasDecoder()
{
return _combinedConfig.has_decoder();
}
std::shared_ptr<Decoder>& Config::getDecoder()
{
if (!_decoder)
{
if (!hasDecoder())
error("no decoder configured");
_decoder = Decoder::create((*this)->decoder());
}
return _decoder;
}
const std::vector<FluxConstructor>& Config::getFluxFormats()
{
return fluxConstructors;
}