diff --git a/doc/digidoc-tool.md b/doc/digidoc-tool.md index 77e58bfa5..f627ef404 100644 --- a/doc/digidoc-tool.md +++ b/doc/digidoc-tool.md @@ -10,14 +10,12 @@ The general format for executing the program is: ``` Available optional options on all commands: - - - - -
\-\-nocolor -Disable terminal colors
\-\-loglevel=[0,1,2,3,4] -Log level: 0 - none, 1 - error, 2 - warning, 3 - info, 4 - debug
\-\-logfile= -File to log, empty to console
+ +| Option | Description | +|--------------------------|------------------------------------------------------------------| +| `--nocolor` | Disable terminal colors | +| `--loglevel=[0,1,2,3,4]` | Log level: 0 - none, 1 - error, 2 - warning, 3 - info, 4 - debug | +| `--logfile=` | File to log, empty to console | ## Creating and signing a document (local signing) {#Creating} @@ -28,18 +26,18 @@ General form of the command is: ``` Available options: - - - - -
\-\-file= Required -Data file(s) to be signed. The option can occur multiple times. -\warning It is recommended not to use special characters in the data file’s name, i.e. it is suggested to only use the characters that are categorized as "unreserved" according to \ref RFC3986 "RFC 3986".
\-\-mime= Optional -Specifies the data file's mime-type value. When used then must be written right after the "--file" parameter. If left unspecified then the default mime-type value "application/octet-stream" is used. +| Option | Usage | Description | +|--------------|----------|--------------------------------------------------| +| `--file=` | Required | Data file to be signed, can occur multiple times | +| `--mime=` | Optional | Mime-type of the preceding `--file` | +| `--dontsign` | Optional | Don't sign the newly created container | + +`--mime=` must be written right after the `--file` parameter it applies to. If left unspecified then the default mime-type value "application/octet-stream" is used. + +\note Data file names are stored percent-encoded (\ref RFC3986 "RFC 3986") in signature references, so special characters in otherwise valid names can be used. Directory separators and reserved names such as `mimetype` are not accepted. Names that use only "unreserved" characters (letters, digits, `-`, `.`, `_`, `~`) are the most portable across other implementations. -\warning Data file’s mime-type value must be formatted as specified in \ref RFC2045 "RFC 2045", section 5.1, i.e. the "type" and "subtype" values must be separated with a forward slash character.
\-\-dontsign Optional -Don't sign the newly created container.
+\warning Data file’s mime-type value must be formatted as specified in \ref RFC2045 "RFC 2045", section 5.1, i.e. the "type" and "subtype" values must be separated with a forward slash character. Additional options for the "create" command are the same as for "sign" command (see \ref Adding). @@ -98,7 +96,7 @@ For additional options look sign command. ## Add additional files to container {#add} Command "add" for adding additional files to existing unsigned container. -Available options are --file and --mime look "create" command for info. +Available options are `--file` and `--mime` look "create" command for info. ## Creating and signing a document (external signing, e.g. in browser) {#websign} @@ -118,14 +116,14 @@ General form of the command is: ``` Available options: - - - - -
\-\-cert= Required -Signer's certificate, in PEM format.
\-\-file= Required -Data file(s) to be signed. The option can occur multiple times.
\-\-mime= Optional -Specifies the data file's mime-type value. When used then must be written right after the "--file" parameter. If left unspecified then the default mime-type value "application/octet-stream" is used.
+ +| Option | Usage | Description | +|-----------|----------|--------------------------------------------------| +| `--cert=` | Required | Signer's certificate, in PEM format | +| `--file=` | Required | Data file to be signed, can occur multiple times | +| `--mime=` | Optional | Mime-type of the preceding `--file` | + +`--mime=` must be written right after the `--file` parameter it applies to. If left unspecified then the default mime-type value "application/octet-stream" is used. Additional options for the "websign" command are the same as for "sign" command (see \ref Adding). @@ -147,33 +145,30 @@ Input: demo-container.asice - container to be created ``` - ## Opening document, validating signatures and extracting data files {#Opening} -Command "open" reads an existing document, prints its contents and validates its signatures. The --extractAll option extracts data files to disk. Supported ASiC-E, ASiC-S and legacy BDOC 2.1 variants are handled locally. PDF, legacy DDOC and ASiC containers with CAdES signatures are sent to the SiVa Online Service for validation; --offline disables that fallback, so those service-backed formats cannot be opened offline. BDOC 1.0 is not supported. +Command "open" reads an existing document, prints its contents and validates its signatures. The `--extractAll` option extracts data files to disk. Supported ASiC-E, ASiC-S and legacy BDOC 2.1 variants are handled locally. PDF, legacy DDOC and ASiC containers with CAdES signatures are sent to the SiVa Online Service for validation; `--offline` disables that fallback, so those service-backed formats cannot be opened offline. BDOC 1.0 is not supported. General form of the command is: ``` > digidoc-tool open ``` Available options: - - - - - -
\-\-extractAll Optional -If set, then all of the input container’s data files are extracted and written to disk without validating signatures. If an output directory is not specified with the value of this parameter then the extracted files are written to the current working directory. - -On Windows, reserved device names are prefixed with an underscore (CON becomes _CON). Characters that are invalid in Windows filenames, control characters, and trailing spaces or dots are replaced with underscores. Extraction fails rather than overwriting an existing output file.
--validateOnExtract Optional -If set, then validates container before extracting files.
\-\-offline Optional -open container offline (eg. Don't send to SiVa)
\-\-warnings= - -(ignore, warning, error) Optional -Enables to choose the displaying of validation warnings (if present) of the file being opened. Can be used to test the warnings system of the utility program (see also "Validation status VALID WITH WARNINGS"). -The options include: -- warning – the default value used. The minor technical errors that are considered as warnings, are printed out as warnings. -- error – the errors that are otherwise considered as warnings (by the utility program), are printed out as errors. -- ignore – the errors that are otherwise considered as warnings (by the utility program), are not printed out. If there are any other errors present then these are treated as usual.
+ +| Option | Usage | Description | +|-------------------------------------|----------|--------------------------------------------------| +| `--extractAll` | Optional | Extract data files without validating signatures | +| `--validateOnExtract` | Optional | Validate the container before extracting files | +| `--offline` | Optional | Open container offline (e.g. don't send to SiVa) | +| `--warnings=(ignore,warning,error)` | Optional | How validation warnings are displayed | + +`--extractAll` writes all of the input container’s data files to disk without validating signatures. If an output directory is not specified with the value of this parameter then the extracted files are written to the current working directory. + +On Windows, reserved device names are prefixed with an underscore (CON becomes _CON). Characters that are invalid in Windows filenames, control characters, and trailing spaces or dots are replaced with underscores. Extraction fails rather than overwriting an existing output file. + +`--warnings` enables to choose the displaying of validation warnings (if present) of the file being opened. Can be used to test the warnings system of the utility program (see also "Validation status VALID WITH WARNINGS"). The options include: +- warning – the default value used. The minor technical errors that are considered as warnings, are printed out as warnings. +- error – the errors that are otherwise considered as warnings (by the utility program), are printed out as errors. +- ignore – the errors that are otherwise considered as warnings (by the utility program), are not printed out. If there are any other errors present then these are treated as usual. Output of the default command contains the following data of the container: ``` @@ -291,71 +286,56 @@ Output: Document(application/octet-stream) extracted to demo/file2.pdf (476841 bytes) ``` - - - ## Adding signatures {#Adding} Command "sign" enables adding XAdES signatures to existing ASiC-E and legacy BDOC 2.1 containers. The same signing options are used by "create" to add the initial \ref RFC3161 "RFC 3161" TimeStampToken to a new ASiC-S container; an existing ASiC-S container cannot receive another ordinary signature. ``` > digidoc-tool sign ``` - - - - - - - - - - - - - - - - - - -
\-\-pin= Optional -If PIN is not provided with this parameter value and (the default) PKCS#11 module is used for signing then the utility program asks for the user to insert PIN code to command line during the program’s execution time.
\-\-profile= Optional -Profile of the signature. Possible values are: +| Option | Usage | Description | +|--------------------------------|----------|---------------------------------------------------------| +| `--pin=` | Optional | PIN code, asked on the command line when omitted | +| `--profile=` | Optional | Signature profile, see the values below | +| `--XAdESEN` | Optional | Use XAdES EN profile | +| `--city=` | Optional | City where the signature is created | +| `--street=` | Optional | streetAddress of production place in XAdES EN profile | +| `--state=` | Optional | State or province where the signature is created | +| `--postalCode=` | Optional | Postal code of the place where the signature is created | +| `--country=` | Optional | Country of origin, ISO 3166 2-character code (e.g. EE) | +| `--role=` | Optional | Signer’s role, can occur multiple times | +| `--sha(224,256,384,512)` | Optional | Hash function for digest values (testing) | +| `--sigsha(224,256,384,512)` | Optional | Hash function for the signed hash (testing) | +| `--sigpsssha(224,256,384,512)` | Optional | Same as `--sigsha*` with `--rsapss` (testing) | +| `--rsapkcs15` | Optional | Change RSA signature padding to PKCS1.5 | +| `--rsapss` | Optional | Change RSA signature padding to PSS | +| `--tsurl=` | Optional | Change TS URL | +| `--userAgent=` | Optional | Application info sent to the TSA or OCSP service | +| `--dontValidate` | Optional | Don't validate container on signature creation | + +If PIN is not provided with `--pin=` and (the default) PKCS#11 module is used for signing then the utility program asks for the user to insert PIN code to command line during the program’s execution time. + +`--profile=` possible values are: - TS or time-stamp - a time-stamp and OCSP confirmation will be added to an XAdES signature as validation data. - TSA or time-stamp-archive - a time-stamp and OCSP confirmation will be added to an XAdES signature as validation data, followed by an archive time-stamp over the certificate and revocation information. - TimeStampToken or time-stamp-token - create the \ref RFC3161 "RFC 3161" token signature used by a new ASiC-S container. -
\-\-XAdESEN Optional -Use XAdES EN profile.
\-\-city= Optional -City where the signature is created.
\-\-street= Optional -streetAddress of production place in XAdES EN profile.
\-\-state= Optional -State or province where the signature is created.
\-\-postalCode= Optional -Postal code of the place where the signature is created.
\-\-country= Optional -Country of origin. ISO 3166-type 2-character country codes are used (e.g. EE)
\-\-role= Optional -Signer’s role(s). The option can occur multiple times.
\-\-sha(224,256,384,512) Optional -Used for testing purposes. Specifies the hash function that is used when calculating digest values. If not specified then SHA-256 is used by default.
\-\-sigsha(224,256,384,512) Optional -Used for testing purposes. Specifies the hash function that is used for calculating the hash that is being signed. If not specified then SHA-256 is used by default.
\-\-sigpsssha(224,256,384,512) Optional -Used for testing purposes. With RSA keys RSA-PSS padding is used. Specifies the hash function that is used for calculating the hash that is being signed. If not specified then SHA-256 is used by default. Same as \-\-sigsha* with \-\-rsapss
\-\-rsapkcs15 Optional -Option to change RSA Signature padding (RSA PKCS1.5).
\-\-rsapss Optional -Option to change RSA Signature padding (RSA PSS).
\-\-tsurl= Optional -Option to change TS URL.
\-\-userAgent= Optional -Additional application information sent to the TSA or OCSP service.
\-\-dontValidate Optional -Don't validate container on signature creation.
+The hash options are used for testing purposes. `--sha*` specifies the hash function that is used when calculating digest values, `--sigsha*` the hash function that is used for calculating the hash that is being signed, and `--sigpsssha*` does the same and uses RSA-PSS padding with RSA keys. If not specified then SHA-256 is used by default. Options for specifying module used for accessing the signing token - possible alternatives are PKCS#11, CryptoAPI/CNG and PKCS#12 (for testing purposes). When signing module is not specified then PKCS#11 module is used by default. - - - - - - -
\-\-pkcs11[=] Optional -Signing is done via PKCS#11 module - the default module for singing with smart card in Linux and macOS. When signing via PKCS#11 module then the parameter’s value can be used to specify the path and filename of PKCS#11 driver in your file system. For example, "opensc-pkcs11.dll" in Windows environment and "opensc-pkcs11.so" in Linux and OSX. -If the parameter’s value is left unspecified then PKCS#11 driver’s location is looked up from configuration file (see also chap. \ref parameters).
\-\-cng Optional -Set the parameter to sign via Microsoft CNG API (in Windows environment). If "--pin" parameter’s value is not set then PIN insertion dialog is displayed to the user. Parameter "--cng" may optionally be used along with parameter "--selectFirst" or "--thumbprint".
\-\-selectFirst Optional -Additional parameter that can optionally be used along with parameter "–cng". When the parameter is set then the first certificate in Windows certificate store is chosen for signature creation. If the parameter is not set then certificate selection dialog window is displayed to user.
\-\-thumbprint Optional -Additional parameter that can optionally be used along with parameter "–cng". When the parameter is set then the certificate by thumbprint in Windows certificate store is chosen for signature creation. If the parameter is not set then certificate selection dialog window is displayed to user.
\-\-pkcs12= Optional -Signing is done via PKCS#12 module - can be used for testing purposes. Enables to use a PKCS#12 software token (containing the signing certificate and private key) for signature creation. Note that the created signature is not a valid signature and it is not equal to handwritten signature as the PKCS#12 software token is not considered a secure signature creation device.
+ +| Option | Usage | Description | +|-----------------|----------|--------------------------------------------------------| +| `--pkcs11[=]` | Optional | Sign via PKCS#11 module (default on Linux and macOS) | +| `--cng` | Optional | Sign via Microsoft CNG API (Windows) | +| `--selectFirst` | Optional | With `--cng`: use the first certificate in the store | +| `--thumbprint=` | Optional | With `--cng`: hex thumbprint of the certificate to use | +| `--pkcs12=` | Optional | Sign with a PKCS#12 software token (testing) | + +PKCS#11 is the default module for signing with smart card in Linux and macOS. The value of `--pkcs11=` can be used to specify the path and filename of PKCS#11 driver in your file system. For example, "opensc-pkcs11.dll" in Windows environment and "opensc-pkcs11.so" in Linux and OSX. If the parameter’s value is left unspecified then PKCS#11 driver’s location is looked up from configuration file (see also chap. \ref parameters). + +With `--cng`, if `--pin` parameter’s value is not set then PIN insertion dialog is displayed to the user. If neither `--selectFirst` nor `--thumbprint` is set then certificate selection dialog window is displayed to user. + +`--pkcs12=` enables to use a PKCS#12 software token (containing the signing certificate and private key) for signature creation. Note that the created signature is not a valid signature and it is not equal to handwritten signature as the PKCS#12 software token is not considered a secure signature creation device. Sample commands for adding signatures: ``` @@ -387,31 +367,28 @@ Input: demo-container.asice - container to be modified ``` - ## Extending container validity {#Extending} -Command "extend" adds new validity evidence and saves the result. Without --signature, it calls the high-level digidoc::Container::extendContainerValidity workflow: eligible ASiC-E or ASiC-S signatures are extended in place, or the original container is wrapped in a new timestamped ASiC-S container when necessary. +Command "extend" adds new validity evidence and saves the result. Without `--signature`, it calls the high-level digidoc::Container::extendContainerValidity workflow: eligible ASiC-E or ASiC-S signatures are extended in place, or the original container is wrapped in a new timestamped ASiC-S container when necessary. ``` > digidoc-tool extend demo-container.asice ``` -When one or more --signature options are specified, only those zero-based signature indexes are extended directly with digidoc::Signature::extendSignatureProfile. In this mode --profile selects the target profile and --dontValidate skips validation after each extension. +When one or more `--signature` options are specified, only those zero-based signature indexes are extended directly with digidoc::Signature::extendSignatureProfile. In this mode `--profile` selects the target profile and `--dontValidate` skips validation after each extension. + +| Option | Usage | Description | +|------------------|----------|-------------------------------------------------------| +| `--signature=` | Optional | Zero-based index of a signature to extend, can repeat | +| `--profile=` | Optional | Target profile for direct per-signature extension | +| `--dontValidate` | Optional | Don't validate a directly extended signature | - - - - -
\-\-signature=Optional -Zero-based signature index to extend directly. The option can occur multiple times.
\-\-profile=Optional -Target profile for direct per-signature extension: TS, TSA, time-stamp or time-stamp-archive. The high-level workflow chooses the required profile automatically when --signature is omitted.
\-\-dontValidateOptional -Do not validate a directly extended signature after extension.
+`--profile=` values are TS, TSA, time-stamp or time-stamp-archive. The high-level workflow chooses the required profile automatically when `--signature` is omitted. ``` Sample: directly extend signature 0 to the archive time-stamp profile > digidoc-tool extend --signature=0 --profile=TSA demo-container.asice ``` - ## Removing signatures and data files {#Removing} Signatures and data files can be removed from a DigiDoc container with the command "remove". Note that it is possible to remove data files only from an unsigned container (i.e all signatures must be removed before removing data files). The command is supported with DigiDoc formats BDOC 2.1 and ASiC-E. General format of the command is: @@ -420,12 +397,12 @@ General format of the command is: ``` Available options: - - - -
\-\-document= Optional -Specifies the sequence number of the data file that is removed from the container. The sequence numbers are counted from zero.
\-\-signature= Optional -Specifies the sequence number of the signature that is removed from the container. The sequence numbers are counted from zero.
+ +| Option | Usage | Description | +|----------------|----------|-----------------------------------------------------| +| `--document=` | Optional | Index of the data file to remove, counted from zero | +| `--signature=` | Optional | Index of the signature to remove, counted from zero | + Sample commands for removing signatures and data files: ``` Sample: removing signature from container diff --git a/src/ASiC_S.cpp b/src/ASiC_S.cpp index 1ad6cbb30..2e5cd9220 100644 --- a/src/ASiC_S.cpp +++ b/src/ASiC_S.cpp @@ -66,7 +66,7 @@ ASiC_S::ASiC_S(const string &path, bool create) continue; else if(const auto directory = File::directory(file); !directory.empty() && directory != "/" && directory != "./") - THROW("Subfolders are not supported %s", directory.c_str()); + THROW("Subfolders are not supported %.*s", STR_VIEW_FMT(directory)); else if(!dataFiles().empty()) THROW("Can not add document to ASiC-S container which already contains a document."); else diff --git a/src/DataFile.cpp b/src/DataFile.cpp index 5cfe26672..b94fda8b1 100644 --- a/src/DataFile.cpp +++ b/src/DataFile.cpp @@ -186,7 +186,15 @@ void DataFilePrivate::saveAs(const string& path) const void DataFilePrivate::saveAs(ostream &os) const { m_is->clear(); - m_is->seekg(0); + bool seekFailed = !m_is->seekg(0); + if(seekFailed) + m_is->clear(); + if(m_is->peek() == istream::traits_type::eof()) + { + if(seekFailed || m_is->bad() || d->size.value_or(0) != 0) + THROW("Failed to read data file '%s'.", m_filename.c_str()); + return; + } os << m_is->rdbuf(); } diff --git a/src/util/File.cpp b/src/util/File.cpp index c7d191ad9..88235b36f 100644 --- a/src/util/File.cpp +++ b/src/util/File.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -187,10 +188,10 @@ string File::frameworkResourcesPath(string_view name) * @param path full path of the file. * @return returns directory part of the file full path. */ -string File::directory(const string& path) +string_view File::directory(string_view path) { size_t pos = path.find_last_of("/\\"); - return pos == string::npos ? string() : path.substr(0, pos); + return pos == string::npos ? string_view() : path.substr(0, pos); } /** @@ -243,12 +244,12 @@ fs::path File::tempFileName() * @param path full path of the directory created. * @throws IOException exception is thrown if the directory creation failed. */ -void File::createDirectory(string path) +void File::createDirectory(string_view path) { if(path.empty()) THROW("Can not create directory with no name."); if(path.back() == '/' || path.back() == '\\') - path.pop_back(); + path.remove_suffix(1); auto _path = encodeName(path); #ifdef _WIN32 int result = _wmkdir(_path.c_str()); @@ -257,13 +258,13 @@ void File::createDirectory(string path) #endif if(result == 0 || errno == EEXIST) { - DEBUG("Created directory or directory exists '%s'", path.c_str()); + DEBUG("Created directory or directory exists '%.*s'", STR_VIEW_FMT(path)); return; } if(errno != ENOENT) - THROW("Failed to create directory '%s', errno = %d", path.c_str(), errno); + THROW("Failed to create directory '%.*s', errno = %d", STR_VIEW_FMT(path), errno); createDirectory(directory(path)); - createDirectory(std::move(path)); + createDirectory(path); } string File::digidocppPath() @@ -318,7 +319,7 @@ string File::toUriPath(const string &path) if(isalnum(i, locC) || unreserved.find(i) != string::npos) dst << i; else - dst << '%' << hex << uppercase << (static_cast(i) & 0xFF); + dst << '%' << hex << uppercase << setw(2) << setfill('0') << (static_cast(i) & 0xFF); } return dst.str(); } @@ -328,7 +329,8 @@ constexpr bool fromHexChar(auto pos, auto end, auto &value) if(distance(pos, end) < 2) return false; auto *p = to_address(pos); - return from_chars(p, p + 2, value, 16).ec == errc{}; + auto [ptr, ec] = from_chars(p, p + 2, value, 16); + return ec == errc{} && ptr == p + 2; } string File::fromUriPath(string_view path) diff --git a/src/util/File.h b/src/util/File.h index a040473d3..a5e5ff969 100644 --- a/src/util/File.h +++ b/src/util/File.h @@ -42,10 +42,10 @@ namespace digidoc static bool fileExtension(std::string_view path, std::initializer_list list); static unsigned long fileSize(const std::filesystem::path &path) noexcept; static std::string_view fileName(std::string_view path) noexcept; - static std::string directory(const std::string& path); + static std::string_view directory(std::string_view path); static std::string path(std::string dir, std::string_view relativePath); static std::filesystem::path tempFileName(); - static void createDirectory(std::string path); + static void createDirectory(std::string_view path); static std::string toUriPath(const std::string &path); static std::string fromUriPath(std::string_view path); static std::vector hexToBin(std::string_view in); diff --git a/test/libdigidocpp_boost.cpp b/test/libdigidocpp_boost.cpp index ee0b9de09..182efe855 100644 --- a/test/libdigidocpp_boost.cpp +++ b/test/libdigidocpp_boost.cpp @@ -212,6 +212,40 @@ BOOST_AUTO_TEST_CASE_TEMPLATE(constructor, Doc, DocTypes) BOOST_CHECK_EQUAL(d->mediaType(), Doc::TYPE); } +BOOST_AUTO_TEST_CASE(emptyDataFile) +{ + auto d = Container::createPtr("empty-datafile.asice"); + d->addDataFile(make_unique(), "empty.txt", "text/plain"); + d->addDataFile(make_unique("data"), "data.txt", "text/plain"); + for(const DataFile *file: d->dataFiles()) + { + ostringstream os; + file->saveAs(os); + BOOST_CHECK(os.good()); + BOOST_CHECK_EQUAL(os.str().size(), file->fileSize()); + } +} + +BOOST_AUTO_TEST_CASE(nonSeekableDataFile) +{ + struct NonSeekableBuf: stringbuf + { + using stringbuf::stringbuf; + pos_type seekoff(off_type, ios_base::seekdir, ios_base::openmode) override { return pos_type(off_type(-1)); } + pos_type seekpos(pos_type, ios_base::openmode) override { return pos_type(off_type(-1)); } + } buf("data"); + + auto d = Container::createPtr("nonseekable-datafile.asice"); + d->addDataFile(make_unique(&buf), "data.txt", "text/plain"); + ostringstream os; + d->dataFiles().front()->saveAs(os); + BOOST_CHECK_EQUAL(os.str(), "data"); + BOOST_CHECK(os.good()); + + ostringstream exhausted; + BOOST_CHECK_THROW(d->dataFiles().front()->saveAs(exhausted), Exception); +} + BOOST_AUTO_TEST_CASE_TEMPLATE(document, Doc, DocTypes) { auto d = Container::createPtr("test." + Doc::EXT); @@ -505,6 +539,15 @@ BOOST_AUTO_TEST_CASE(XmlConfCase) { BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE(FileUtilSuite) +BOOST_AUTO_TEST_CASE(ToUriPathEscapesSingleDigitBytes) +{ + const string fileName = "file\n\t.txt"; + const string uriPath = util::File::toUriPath(fileName); + + BOOST_CHECK_EQUAL(uriPath, "file%0A%09.txt"); + BOOST_CHECK_EQUAL(util::File::fromUriPath(uriPath), fileName); +} + BOOST_AUTO_TEST_CASE(FromUriPathConvertsAsciiEncodingToCharacters) { const string asciiEncodedStr = "%3dtest%20%40"; @@ -535,6 +578,11 @@ BOOST_AUTO_TEST_CASE(FromUriPathPreservesTrailingPercentageSign) BOOST_CHECK_EQUAL(expectedDecodedStr, result); } +BOOST_AUTO_TEST_CASE(FromUriPathPreservesInvalidEscape) +{ + BOOST_CHECK_EQUAL(util::File::fromUriPath("file%A.txt"), "file%A.txt"); +} + BOOST_AUTO_TEST_CASE(HexToBinConvertsHexEncodedStringToBinaryData) { const string asciiEncodedStr = "3031323334";