diff options
-rw-r--r-- | hscript/meta.cc | 92 | ||||
-rw-r--r-- | hscript/meta.hh | 7 | ||||
-rw-r--r-- | hscript/script.cc | 42 | ||||
-rw-r--r-- | tests/fixtures/0131-timezone-basic.installfile | 6 | ||||
-rw-r--r-- | tests/fixtures/0132-timezone-invalid.installfile | 7 | ||||
-rw-r--r-- | tests/fixtures/0133-timezone-malicious.installfile | 7 | ||||
-rw-r--r-- | tests/fixtures/0134-timezone-subtz.installfile | 6 | ||||
-rw-r--r-- | tests/spec/validator_spec.rb | 78 |
8 files changed, 210 insertions, 35 deletions
diff --git a/hscript/meta.cc b/hscript/meta.cc index 79881f4..9f0da76 100644 --- a/hscript/meta.cc +++ b/hscript/meta.cc @@ -19,8 +19,8 @@ # include <cstring> /* strerror */ # include <errno.h> /* errno */ # include <sys/stat.h> /* chmod */ -# include <unistd.h> #endif /* HAS_INSTALL_ENV */ +#include <unistd.h> /* access - used by tz code even in RT env */ #include "meta.hh" #include "util/output.hh" @@ -183,6 +183,20 @@ Key *PkgInstall::parseFromData(const std::string &data, int lineno, int *errors, } +/* LCOV_EXCL_START */ +bool PkgInstall::validate(ScriptOptions) const { + /* Any validation errors would have occurred above. */ + return true; +} + + +bool PkgInstall::execute(ScriptOptions) const { + /* Package installation is handled in Script::execute. */ + return true; +} +/* LCOV_EXCL_STOP */ + + /* All ISO 639-1 language codes. * Source: https://www.loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt * Python to construct table: @@ -318,18 +332,78 @@ bool Firmware::execute(ScriptOptions) const { } -/* LCOV_EXCL_START */ -bool PkgInstall::validate(ScriptOptions) const { - /* Any validation errors would have occurred above. */ - return true; +Key *Timezone::parseFromData(const std::string &data, int lineno, int *errors, + int *warnings) { + if(data.find_first_of(" .\\") != std::string::npos || data[0] == '/') { + if(errors) *errors += 1; + output_error("installfile:" + std::to_string(lineno), + "timezone: invalid timezone name"); + return nullptr; + } + + if(access("/usr/share/zoneinfo", X_OK) != 0) { + if(warnings) *warnings += 1; + output_warning("installfile:" + std::to_string(lineno), + "timezone: can't determine validity of timezone", + "zoneinfo data is missing or inaccessible"); + } else { + std::string zi_path = "/usr/share/zoneinfo/" + data; + if(access(zi_path.c_str(), F_OK) != 0) { + if(errors) *errors += 1; + output_error("installfile:" + std::to_string(lineno), + "timezone: unknown timezone '" + data + "'"); + return nullptr; + } + } + + return new Timezone(lineno, data); } +bool Timezone::execute(ScriptOptions opts) const { + if(opts.test(Simulate)) { + /* If the target doesn't have tzdata installed, copy the zoneinfo from + * the Horizon environment. */ + std::cout << "([ -f /target/usr/share/zoneinfo/" << this->value() + << " ] && ln -s /usr/share/zoneinfo/" << this->value() + << " /target/etc/localtime) || cp /usr/share/zoneinfo/" + << this->value() << " /target/etc/localtime"; + return true; + } -bool PkgInstall::execute(ScriptOptions) const { - /* Package installation is handled in Script::execute. */ - return true; +#ifdef HAS_INSTALL_ENV + std::string zi_path = "/usr/share/zoneinfo/" + this->value(); + std::string target_zi = "/target" + zi_path; + if(access(target_zi.c_str(), F_OK) == 0) { + if(symlink(zi_path.c_str(), "/target/etc/localtime") != 0) { + output_error("installfile:" + std::to_string(this->lineno()), + "timezone: failed to create symbolic link", + strerror(errno)); + return false; + } + return true; + } else { + /* The target doesn't have tzdata installed. We copy the zoneinfo + * file from the Horizon environment to the target. */ + std::ifstream zoneinfo(zi_path, std::ios::binary); + if(!zoneinfo) { + output_error("installfile:" + std::to_string(this->lineno()), + "timezone: failed to open zoneinfo file"); + return false; + } + std::ofstream output("/target/etc/localtime"); + if(!output) { + output_error("installfile:" + std::to_string(this->lineno()), + "timezone: failed to prepare target environment"); + return false; + } + + output << zoneinfo.rdbuf(); + return true; + } +#else + return false; +#endif } -/* LCOV_EXCL_STOP */ Key *Repository::parseFromData(const std::string &data, int lineno, int *errors, diff --git a/hscript/meta.hh b/hscript/meta.hh index 7dc3f68..b2e1963 100644 --- a/hscript/meta.hh +++ b/hscript/meta.hh @@ -66,6 +66,13 @@ public: }; class Timezone : public StringKey { +private: + Timezone(int _line, const std::string &my_zone) : + StringKey(_line, my_zone) {} +public: + static Key *parseFromData(const std::string &data, int lineno, int *errors, + int *warnings); + bool execute(ScriptOptions) const override; }; class Repository : public StringKey { diff --git a/hscript/script.cc b/hscript/script.cc index 0e99059..63ea3de 100644 --- a/hscript/script.cc +++ b/hscript/script.cc @@ -90,6 +90,9 @@ struct Script::ScriptPrivate { std::unique_ptr<RootPassphrase> rootpw; /*! The system language. */ std::unique_ptr<Language> lang; + /* keymap */ + /*! The system timezone. */ + std::unique_ptr<Timezone> tzone; /*! Network addressing configuration */ std::vector< std::unique_ptr<NetAddress> > addresses; @@ -141,6 +144,8 @@ struct Script::ScriptPrivate { return store_lang(obj, lineno, errors, warnings, opts); } else if(key_name == "firmware") { return store_firmware(obj, lineno, errors, warnings, opts); + } else if(key_name == "timezone") { + return store_timezone(obj, lineno, errors, warnings, opts); } else if(key_name == "repository") { std::unique_ptr<Repository> repo(dynamic_cast<Repository *>(obj)); this->repos.push_back(std::move(repo)); @@ -263,6 +268,18 @@ struct Script::ScriptPrivate { return true; } + bool store_timezone(Keys::Key *obj, int lineno, int *errors, int *, + ScriptOptions) { + if(this->tzone) { + DUPLICATE_ERROR(this->tzone, std::string("timezone"), + this->tzone->value()) + return false; + } + std::unique_ptr<Timezone> t(dynamic_cast<Timezone *>(obj)); + this->tzone = std::move(t); + return true; + } + bool store_username(Keys::Key *obj, int lineno, int *errors, int *, ScriptOptions) { if(accounts.size() >= 255) { @@ -560,7 +577,22 @@ bool Script::validate() const { if(!this->internal->firmware->validate(this->opts)) failures++; #endif - /* timezone */ + /* REQ: Runner.Execute.timezone */ + if(!internal->tzone) { + Keys::Timezone *utc = dynamic_cast<Keys::Timezone *>( + Horizon::Keys::Timezone::parseFromData("UTC", 0, + &failures, nullptr) + ); + if(!utc) { + output_error("internal", "failed to create default timezone"); + return false; + } + std::unique_ptr<Keys::Timezone> zone(utc); + this->internal->tzone = std::move(zone); + } + + /* REQ: Runner.Validate.timezone */ + if(!this->internal->tzone->validate(this->opts)) failures++; /* REQ: Script.repository */ if(this->internal->repos.size() == 0) { @@ -900,6 +932,14 @@ bool Script::execute() const { return false; } + /* keymap */ + /* UserAccounts */ + + if(!this->internal->tzone->execute(opts)) { + EXECUTE_FAILURE("post-metadata"); + return false; + } + output_step_end("post-metadata"); return true; } diff --git a/tests/fixtures/0131-timezone-basic.installfile b/tests/fixtures/0131-timezone-basic.installfile new file mode 100644 index 0000000..ca8306a --- /dev/null +++ b/tests/fixtures/0131-timezone-basic.installfile @@ -0,0 +1,6 @@ +network false +hostname test.machine +pkginstall adelie-base +rootpw $6$gumtLGmHwOVIRpQR$2M9PUO24hy5mofzWWf9a.YLbzOgOlUby1g0hDj.wG67E2wrrvys59fq02PPdxBdbgkLZFtjfEx6MHZwMBamwu/ +mount /dev/sda1 / +timezone UTC diff --git a/tests/fixtures/0132-timezone-invalid.installfile b/tests/fixtures/0132-timezone-invalid.installfile new file mode 100644 index 0000000..53e8446 --- /dev/null +++ b/tests/fixtures/0132-timezone-invalid.installfile @@ -0,0 +1,7 @@ +network false +hostname test.machine +pkginstall adelie-base +rootpw $6$gumtLGmHwOVIRpQR$2M9PUO24hy5mofzWWf9a.YLbzOgOlUby1g0hDj.wG67E2wrrvys59fq02PPdxBdbgkLZFtjfEx6MHZwMBamwu/ +mount /dev/sda1 / +# This is in Africa, not America. +timezone America/Nairobi diff --git a/tests/fixtures/0133-timezone-malicious.installfile b/tests/fixtures/0133-timezone-malicious.installfile new file mode 100644 index 0000000..519d3ec --- /dev/null +++ b/tests/fixtures/0133-timezone-malicious.installfile @@ -0,0 +1,7 @@ +network false +hostname test.machine +pkginstall adelie-base +rootpw $6$gumtLGmHwOVIRpQR$2M9PUO24hy5mofzWWf9a.YLbzOgOlUby1g0hDj.wG67E2wrrvys59fq02PPdxBdbgkLZFtjfEx6MHZwMBamwu/ +mount /dev/sda1 / +# Try to get the Runner to copy /etc/passwd to /target/etc/localtime +timezone ../../../etc/passwd diff --git a/tests/fixtures/0134-timezone-subtz.installfile b/tests/fixtures/0134-timezone-subtz.installfile new file mode 100644 index 0000000..72af414 --- /dev/null +++ b/tests/fixtures/0134-timezone-subtz.installfile @@ -0,0 +1,6 @@ +network false +hostname test.machine +pkginstall adelie-base +rootpw $6$gumtLGmHwOVIRpQR$2M9PUO24hy5mofzWWf9a.YLbzOgOlUby1g0hDj.wG67E2wrrvys59fq02PPdxBdbgkLZFtjfEx6MHZwMBamwu/ +mount /dev/sda1 / +timezone Pacific/Galapagos diff --git a/tests/spec/validator_spec.rb b/tests/spec/validator_spec.rb index b07e383..457aa93 100644 --- a/tests/spec/validator_spec.rb +++ b/tests/spec/validator_spec.rb @@ -160,6 +160,44 @@ RSpec.describe 'HorizonScript validation', :type => :aruba do run_validate expect(last_command_started).to have_output(/error: .*rootpw.*/) end + context "for 'language' key" do + # Runner.Validate.language + # Runner.Validate.language.Format + it "supports a simple language" do + use_fixture '0115-language-simple.installfile' + run_validate + expect(last_command_started).to have_output(PARSER_SUCCESS) + expect(last_command_started).to have_output(VALIDATOR_SUCCESS) + end + it "supports a language_territory value" do + use_fixture '0116-language-country.installfile' + run_validate + expect(last_command_started).to have_output(PARSER_SUCCESS) + expect(last_command_started).to have_output(VALIDATOR_SUCCESS) + end + it "supports the UTF-8 codeset" do + use_fixture '0117-language-codeset.installfile' + run_validate + expect(last_command_started).to have_output(PARSER_SUCCESS) + expect(last_command_started).to have_output(VALIDATOR_SUCCESS) + end + it "supports the special case C.UTF-8" do + use_fixture '0118-language-c-exception.installfile' + run_validate + expect(last_command_started).to have_output(PARSER_SUCCESS) + expect(last_command_started).to have_output(VALIDATOR_SUCCESS) + end + it "requires a valid language" do + use_fixture '0119-language-invalid.installfile' + run_validate + expect(last_command_started).to have_output(/error: .*language.*invalid/) + end + it "requires the UTF-8 codeset" do + use_fixture '0120-language-iso.installfile' + run_validate + expect(last_command_started).to have_output(/error: .*language.*codeset/) + end + end context "for 'firmware' key" do it "always supports 'false' value" do use_fixture '0112-firmware-false.installfile' @@ -186,43 +224,33 @@ RSpec.describe 'HorizonScript validation', :type => :aruba do expect(last_command_started).to have_output(/error: .*firmware.*value/) end end - context "for 'language' key" do - # Runner.Validate.language - # Runner.Validate.language.Format - it "supports a simple language" do - use_fixture '0115-language-simple.installfile' + context "for 'timezone' key" do + # Runner.Validate.timezone. + it "fails with an invalid value" do + use_fixture '0132-timezone-invalid.installfile' run_validate - expect(last_command_started).to have_output(PARSER_SUCCESS) - expect(last_command_started).to have_output(VALIDATOR_SUCCESS) + expect(last_command_started).to have_output(/error: .*timezone.*invalid/) end - it "supports a language_territory value" do - use_fixture '0116-language-country.installfile' + # Runner.Validate.timezone. + it "fails with a maliciously invalid value" do + use_fixture '0133-timezone-malicious.installfile' run_validate - expect(last_command_started).to have_output(PARSER_SUCCESS) - expect(last_command_started).to have_output(VALIDATOR_SUCCESS) + expect(last_command_started).to have_output(/error: .*timezone.*invalid/) end - it "supports the UTF-8 codeset" do - use_fixture '0117-language-codeset.installfile' + # Runner.Validate.timezone.zoneinfo. + it "succeeds with simple value" do + use_fixture '0131-timezone-basic.installfile' run_validate expect(last_command_started).to have_output(PARSER_SUCCESS) expect(last_command_started).to have_output(VALIDATOR_SUCCESS) end - it "supports the special case C.UTF-8" do - use_fixture '0118-language-c-exception.installfile' + # Runner.Validate.timezone.zoneinfo. + it "succeeds with Locality/Zone value" do + use_fixture '0134-timezone-subtz.installfile' run_validate expect(last_command_started).to have_output(PARSER_SUCCESS) expect(last_command_started).to have_output(VALIDATOR_SUCCESS) end - it "requires a valid language" do - use_fixture '0119-language-invalid.installfile' - run_validate - expect(last_command_started).to have_output(/error: .*language.*invalid/) - end - it "requires the UTF-8 codeset" do - use_fixture '0120-language-iso.installfile' - run_validate - expect(last_command_started).to have_output(/error: .*language.*codeset/) - end end context "for 'mount' key" do # Runner.Validate.mount. |