diff options
author | Sven Gothel <[email protected]> | 2022-06-04 12:17:49 +0200 |
---|---|---|
committer | Sven Gothel <[email protected]> | 2022-06-04 12:17:49 +0200 |
commit | c968b79825902597f85c235e3e20b61e1a25e24b (patch) | |
tree | 6d5d4b8d66a5fb8663cbeb49766cafae0b2b4040 | |
parent | 4239b2623dc979c10eeeda32467d20bd04363a3d (diff) |
Bump v0.13.0: jau::io changes for robust and simplified ByteInStream usage sourced as file or via remote URLv0.13.0
* `string_util.hpp`: Add `jau::to_string()` support for `std::string` and `std::string_view` as well as for `std::vector<T>` lists
* Add namespace `jau::io::uri`, limited URI scheme functionality to query whether implementation may handle the protocol.
- Query *libcurl* supported protocols at runtime
- Test for local file protocol
- Test whether protocol in given uri is supported by *libcurl*
* `jau::io::read_url_stream()`, sync and async, return immediately if protocol in given url is not supportet
- async variant returns `std::unique_ptr<std::thread>`, where a nullptr is used for no support
* `jau::io::ByteInStream_File` recognizes the local file protocol and cuts off `file://` is used.
- Fix: Recognition of a non-existing path, unaccessbile path or non-file case properly
* `jau::io::ByteInStream_URL` recognizes a non supported protocol via async `jau::io::read_url_stream()`.
* Added convenient `jau::io::std::unique_ptr<ByteInStream> to_ByteInStream()`
- Returning either a `jau::io::ByteInStream_File`, `jau::io::ByteInStream_URL` or nullptr if `path_or_url` is not supported
* Make Java class `org.jau.ney.Uri` standalone, drop dependencies for easier reusage.
-rw-r--r-- | README.md | 16 | ||||
-rw-r--r-- | include/jau/byte_stream.hpp | 36 | ||||
-rw-r--r-- | include/jau/io_util.hpp | 72 | ||||
-rw-r--r-- | include/jau/string_util.hpp | 48 | ||||
-rw-r--r-- | java_net/org/jau/net/Uri.java | 106 | ||||
-rw-r--r-- | src/byte_stream.cpp | 80 | ||||
-rw-r--r-- | src/io_util.cpp | 76 | ||||
-rw-r--r-- | test/test_bytestream01.cpp | 148 | ||||
-rw-r--r-- | test/test_iostream01.cpp | 90 |
9 files changed, 595 insertions, 77 deletions
@@ -159,6 +159,22 @@ a Raspi-arm64, Raspi-armhf or PC-amd64 target image. * First stable release (TODO) +**0.13.0** + +* `string_util.hpp`: Add `jau::to_string()` support for `std::string` and `std::string_view` as well as for `std::vector<T>` lists +* Add namespace `jau::io::uri`, limited URI scheme functionality to query whether implementation may handle the protocol. + - Query *libcurl* supported protocols at runtime + - Test for local file protocol + - Test whether protocol in given uri is supported by *libcurl* +* `jau::io::read_url_stream()`, sync and async, return immediately if protocol in given url is not supportet + - async variant returns `std::unique_ptr<std::thread>`, where a nullptr is used for no support +* `jau::io::ByteInStream_File` recognizes the local file protocol and cuts off `file://` is used. + - Fix: Recognition of a non-existing path, unaccessbile path or non-file case properly +* `jau::io::ByteInStream_URL` recognizes a non supported protocol via async `jau::io::read_url_stream()`. +* Added convenient `jau::io::std::unique_ptr<ByteInStream> to_ByteInStream()` + - Returning either a `jau::io::ByteInStream_File`, `jau::io::ByteInStream_URL` or nullptr if `path_or_url` is not supported +* Make Java class `org.jau.ney.Uri` standalone, drop dependencies for easier reusage. + **0.12.0** * Minor changes diff --git a/include/jau/byte_stream.hpp b/include/jau/byte_stream.hpp index e68c431..f15c292 100644 --- a/include/jau/byte_stream.hpp +++ b/include/jau/byte_stream.hpp @@ -42,6 +42,8 @@ // Include Botan header files before this one to be integrated w/ Botan! // #include <botan_all.h> +using namespace jau::fractions_i64_literals; + namespace jau::io { /** \addtogroup IOUtils @@ -365,15 +367,19 @@ namespace jau::io { size_t peek(uint8_t[], size_t, size_t) const NOEXCEPT_BOTAN override; bool check_available(size_t n) NOEXCEPT_BOTAN override; bool end_of_data() const NOEXCEPT_BOTAN override; - bool error() const noexcept override { return m_source.bad(); } + bool error() const noexcept override { return nullptr == m_source || m_source->bad(); } std::string id() const NOEXCEPT_BOTAN override; /** * Construct a Stream-Based byte input stream from filesystem path - * @param file the path to the file + * + * In case the given path is a local file URI starting with `file://`, see jau::io::uri::is_local_file_protocol(), + * the leading `file://` is cut off and the remainder being used. + * + * @param path the path to the file, maybe a local file URI * @param use_binary whether to treat the file as binary (default) or use platform character conversion */ - ByteInStream_File(const std::string& file, bool use_binary = true) noexcept; + ByteInStream_File(const std::string& path, bool use_binary = true) noexcept; ByteInStream_File(const ByteInStream_File&) = delete; @@ -395,7 +401,7 @@ namespace jau::io { private: const std::string m_identifier; - mutable std::ifstream m_source; + mutable std::unique_ptr<std::ifstream> m_source; uint64_t m_content_size; uint64_t m_bytes_consumed; }; @@ -403,7 +409,9 @@ namespace jau::io { /** * This class represents a Ringbuffer-Based byte input stream with a URL connection provisioned data feed. * - * Standard implementation uses curl, hence all protocols supported by curl are supported. + * Standard implementation uses [curl](https://curl.se/), + * hence all [*libcurl* network protocols](https://curl.se/docs/url-syntax.html) are supported, + * jau::io::uri::supported_protocols(). */ class ByteInStream_URL final : public ByteInStream { public: @@ -450,7 +458,7 @@ namespace jau::io { /** * Construct a ringbuffer backed Http byte input stream * @param url the URL of the data to read - * @param timeout maximum duration in fractions of seconds to wait @ check_available(), where fractions_i64::zero waits infinitely + * @param timeout maximum duration in fractions of seconds to wait @ check_available() for next bytes, where fractions_i64::zero waits infinitely */ ByteInStream_URL(const std::string& url, const jau::fraction_i64& timeout) noexcept; @@ -483,11 +491,25 @@ namespace jau::io { jau::relaxed_atomic_uint64 m_content_size; jau::relaxed_atomic_uint64 m_total_xfered; relaxed_atomic_async_io_result_t m_result; - std::thread m_url_thread; + std::unique_ptr<std::thread> m_url_thread; uint64_t m_bytes_consumed; }; /** + * Parses the given path_or_uri, if it matches a supported protocol, see jau::io::uri::protocol_supported(), + * but is not a local file, see jau::io::uri::is_local_file_protocol(), ByteInStream_URL is being attempted. + * + * If the above fails, ByteInStream_File is attempted. + * + * If non of the above leads to a ByteInStream without ByteInStream::error(), nullptr is returned. + * + * @param path_or_uri given path or uri for with a ByteInStream instance shall be established. + * @param timeout a timeout in case ByteInStream_URL is being used as maximum dureation to wait for next bytes at ByteInStream_URL::check_available(), defaults to 20_s + * @return a working ByteInStream w/o ByteInStream::error() or nullptr + */ + std::unique_ptr<ByteInStream> to_ByteInStream(const std::string& path_or_uri, jau::fraction_i64 timeout=20_s) noexcept; + + /** * This class represents a Ringbuffer-Based byte input stream with an externally provisioned data feed. */ class ByteInStream_Feed final : public ByteInStream { diff --git a/include/jau/io_util.hpp b/include/jau/io_util.hpp index c74a875..2ca8475 100644 --- a/include/jau/io_util.hpp +++ b/include/jau/io_util.hpp @@ -84,14 +84,63 @@ namespace jau::io { StreamConsumerFunc consumer_fn) noexcept; /** + * Limited URI scheme functionality to query whether implementation may handle the protocol. + * + * The URI scheme functionality exposed here is limited and only provided to decide whether the used implementation + * is able to handle the protocol. This is not a replacement for a proper URI class. + */ + namespace uri { + /** + * Returns a list of supported protocol supported by [*libcurl* network protocols](https://curl.se/docs/url-syntax.html), + * queried at runtime. + * @see protocol_supported() + */ + std::vector<std::string_view> supported_protocols() noexcept; + + /** + * Returns the valid uri-scheme-view from given uri-view, + * which is empty if no valid scheme is included. + * + * @param uri an uri-view + * @return valid uri-scheme-view, empty if non found + */ + std::string_view get_scheme(const std::string_view& uri) noexcept; + + /** + * Returns true if the uri-scheme of given uri-view matches a supported by [*libcurl* network protocols](https://curl.se/docs/url-syntax.html) otherwise false. + * + * The *libcurl* supported protocols is queried at runtime, see supported_protocols(). + * + * @param uri an uri-view to test + * @return true if the uri-scheme of given uri is supported, otherwise false. + * @see supported_protocols() + * @see get_scheme() + */ + bool protocol_supported(const std::string_view& uri) noexcept; + + /** + * Returns true if the uri-scheme of given uri-view matches the local `file` protocol, i.e. starts with `file://`. + * @param uri an uri-view to test + */ + bool is_local_file_protocol(const std::string_view& uri) noexcept; + } + + /** * Synchronous URL stream reader using the given StreamConsumerFunc consumer_fn. * * To abort streaming, user may return `false` from the given `consumer_func`. * + * Standard implementation uses [curl](https://curl.se/), + * hence all [*libcurl* network protocols](https://curl.se/docs/url-syntax.html) are supported, + * see jau::io::uri::supported_protocols(). + * + * If the uri-sheme doesn't match a supported protocol, see jau::io::uri::protocol_supported(), + * function returns immediately with zero bytes. + * * @param url the URL to open a connection to and stream bytes from * @param buffer secure std::vector buffer, passed down to consumer_fn * @param consumer_fn StreamConsumerFunc consumer for each received heap of bytes, returning true to continue stream of false to abort. - * @return total bytes read or 0 if error + * @return total bytes read or 0 if transmission error or protocol of given url is not supported */ uint64_t read_url_stream(const std::string& url, secure_vector<uint8_t>& buffer, @@ -102,20 +151,27 @@ namespace jau::io { * * To abort streaming, user may set given reference `results` to a value other than async_io_result_t::NONE. * + * Standard implementation uses [curl](https://curl.se/), + * hence all [*libcurl* network protocols](https://curl.se/docs/url-syntax.html) are supported, + * see jau::io::uri::supported_protocols(). + * + * If the uri-sheme doesn't match a supported protocol, see jau::io::uri::protocol_supported(), + * function returns with nullptr. + * * @param url the URL to open a connection to and stream bytes from * @param buffer the ringbuffer destination to write into * @param has_content_length indicating whether content_length is known from server * @param content_length tracking the content_length * @param total_read tracking the total_read * @param result reference to tracking async_io_result_t. If set to other than async_io_result_t::NONE while streaming, streaming is aborted. - * @return the url background reading thread + * @return the url background reading thread unique-pointer or nullptr if protocol of given url is not supported */ - std::thread read_url_stream(const std::string& url, - ByteRingbuffer& buffer, - jau::relaxed_atomic_bool& has_content_length, - jau::relaxed_atomic_uint64& content_length, - jau::relaxed_atomic_uint64& total_read, - relaxed_atomic_async_io_result_t& result) noexcept; + std::unique_ptr<std::thread> read_url_stream(const std::string& url, + ByteRingbuffer& buffer, + jau::relaxed_atomic_bool& has_content_length, + jau::relaxed_atomic_uint64& content_length, + jau::relaxed_atomic_uint64& total_read, + relaxed_atomic_async_io_result_t& result) noexcept; void print_stats(const std::string& prefix, const uint64_t& out_bytes_total, const jau::fraction_i64& td) noexcept; diff --git a/include/jau/string_util.hpp b/include/jau/string_util.hpp index 6673171..8a6a37f 100644 --- a/include/jau/string_util.hpp +++ b/include/jau/string_util.hpp @@ -201,9 +201,31 @@ namespace jau { { return std::to_string(ref); } + + template< class value_type, + std::enable_if_t<!std::is_integral_v<value_type> && + !std::is_floating_point_v<value_type> && + std::is_base_of_v<std::string, value_type>, + bool> = true> + inline std::string to_string(const value_type & ref) { + return ref; + } + + template< class value_type, + std::enable_if_t<!std::is_integral_v<value_type> && + !std::is_floating_point_v<value_type> && + !std::is_base_of_v<std::string, value_type> && + std::is_base_of_v<std::string_view, value_type>, + bool> = true> + inline std::string to_string(const value_type & ref) { + return std::string(ref); + } + template< class value_type, std::enable_if_t<!std::is_integral_v<value_type> && !std::is_floating_point_v<value_type> && + !std::is_base_of_v<std::string, value_type> && + !std::is_base_of_v<std::string_view, value_type> && std::is_pointer_v<value_type>, bool> = true> inline std::string to_string(const value_type & ref) @@ -214,6 +236,8 @@ namespace jau { template< class value_type, std::enable_if_t<!std::is_integral_v<value_type> && !std::is_floating_point_v<value_type> && + !std::is_base_of_v<std::string, value_type> && + !std::is_base_of_v<std::string_view, value_type> && !std::is_pointer_v<value_type> && jau::has_toString_v<value_type>, bool> = true> @@ -224,6 +248,8 @@ namespace jau { template< class value_type, std::enable_if_t<!std::is_integral_v<value_type> && !std::is_floating_point_v<value_type> && + !std::is_base_of_v<std::string, value_type> && + !std::is_base_of_v<std::string_view, value_type> && !std::is_pointer_v<value_type> && !jau::has_toString_v<value_type> && jau::has_to_string_v<value_type>, @@ -235,6 +261,8 @@ namespace jau { template< class value_type, std::enable_if_t<!std::is_integral_v<value_type> && !std::is_floating_point_v<value_type> && + !std::is_base_of_v<std::string, value_type> && + !std::is_base_of_v<std::string_view, value_type> && !std::is_pointer_v<value_type> && !jau::has_toString_v<value_type> && !jau::has_to_string_v<value_type> && @@ -247,6 +275,8 @@ namespace jau { template< class value_type, std::enable_if_t<!std::is_integral_v<value_type> && !std::is_floating_point_v<value_type> && + !std::is_base_of_v<std::string, value_type> && + !std::is_base_of_v<std::string_view, value_type> && !std::is_pointer_v<value_type> && !jau::has_toString_v<value_type> && !jau::has_to_string_v<value_type> && @@ -257,6 +287,24 @@ namespace jau { return "jau::to_string<T> not available for "+type_cue<value_type>::print("unknown", TypeTraitGroup::ALL); } + template<typename T> + std::string to_string(std::vector<T> const &list, const std::string& delim) + { + if ( list.empty() ) { + return std::string(); + } + bool need_delim = false; + std::string res; + for(const T& e : list) { + if( need_delim ) { + res.append( delim ).append( " " ); + } + res.append( to_string( e ) ); + need_delim = true; + } + return res; + } + /**@}*/ } // namespace jau diff --git a/java_net/org/jau/net/Uri.java b/java_net/org/jau/net/Uri.java index c243e8e..f4fb09b 100644 --- a/java_net/org/jau/net/Uri.java +++ b/java_net/org/jau/net/Uri.java @@ -36,10 +36,6 @@ import java.net.URISyntaxException; import java.util.StringTokenizer; import java.util.regex.Pattern; -import org.jau.io.IOUtil; -import org.jau.sys.Debug; -import org.jau.sys.PropertyAccess; - /** * This class implements an immutable Uri as defined by <a href="https://tools.ietf.org/html/rfc2396">RFC 2396</a>. * <p> @@ -164,6 +160,9 @@ import org.jau.sys.PropertyAccess; * @since 0.3.0 */ public class Uri { + private static final boolean DEBUG = false; + private static final boolean DEBUG_SHOWFIX = false; + /** private static final boolean DEBUG; private static final boolean DEBUG_SHOWFIX; @@ -171,7 +170,7 @@ public class Uri { Debug.initSingleton(); DEBUG = IOUtil.DEBUG || Debug.debug("Uri"); DEBUG_SHOWFIX = PropertyAccess.isPropertyDefined("jau.debug.Uri.ShowFix", true); - } + } */ /** * Usually used to fix a path from a previously contained and opaque Uri, @@ -1125,7 +1124,7 @@ public class Uri { * specification RFC2396 or could not be parsed correctly. */ public static Uri valueOf(final File file) throws URISyntaxException { - return Uri.valueOfFilepath(IOUtil.slashify(file.getAbsolutePath(), true, file.isDirectory())); + return Uri.valueOfFilepath(Util.slashify(file.getAbsolutePath(), true, file.isDirectory())); } /** @@ -1457,7 +1456,7 @@ public class Uri { return false; // nothing to cut-off } pathBuf.setLength(0); - pathBuf.append( IOUtil.cleanPathString( pathS ) ); + pathBuf.append( Util.cleanPathString( pathS ) ); cleaned = pathBuf.length() != pathS.length(); } @@ -1489,7 +1488,7 @@ public class Uri { // 2nd round of cleaning! final String pathS = pathBuf.toString(); pathBuf.setLength(0); - pathBuf.append( IOUtil.cleanPathString( pathS ) ); + pathBuf.append( Util.cleanPathString( pathS ) ); } return true; // continue processing w/ buffer } @@ -2543,4 +2542,95 @@ public class Uri { private static void failExpecting(final Encoded input, final String expected, final int p) throws URISyntaxException { fail(input, "Expecting " + expected, p); } + + static class Util { + private static final Pattern patternSingleBS = Pattern.compile("\\\\{1}"); + /** + * + * @param path + * @param startWithSlash + * @param endWithSlash + * @return + * @throws URISyntaxException if path is empty or has no parent directory available while resolving <code>../</code> + */ + public static String slashify(final String path, final boolean startWithSlash, final boolean endWithSlash) throws URISyntaxException { + String p = patternSingleBS.matcher(path).replaceAll("/"); + if (startWithSlash && !p.startsWith("/")) { + p = "/" + p; + } + if (endWithSlash && !p.endsWith("/")) { + p = p + "/"; + } + return cleanPathString(p); + } + /** + * @param path assuming a slashified path, either denotes a file or directory, either relative or absolute. + * @return parent of path + * @throws URISyntaxException if path is empty or has no parent directory available + */ + public static String getParentOf(final String path) throws URISyntaxException { + final int pl = null!=path ? path.length() : 0; + if(pl == 0) { + throw new IllegalArgumentException("path is empty <"+path+">"); + } + + final int e = path.lastIndexOf("/"); + if( e < 0 ) { + throw new URISyntaxException(path, "path contains no '/': <"+path+">"); + } + if( e == 0 ) { + // path is root directory + throw new URISyntaxException(path, "path has no parents: <"+path+">"); + } + if( e < pl - 1 ) { + // path is file, return it's parent directory + return path.substring(0, e+1); + } + final int j = path.lastIndexOf("!") + 1; // '!' Separates JARFile entry -> local start of path + // path is a directory .. + final int p = path.lastIndexOf("/", e-1); + if( p >= j) { + // parent itself has '/' - post '!' or no '!' at all + return path.substring(0, p+1); + } else { + // parent itself has no '/' + final String parent = path.substring(j, e); + if( parent.equals("..") ) { + throw new URISyntaxException(path, "parent is unresolved: <"+path+">"); + } else { + // parent is '!' or empty (relative path) + return path.substring(0, j); + } + } + } + + /** + * @param path assuming a slashified path, either denoting a file or directory, either relative or absolute. + * @return clean path string where {@code ./} and {@code ../} is resolved, + * while keeping a starting {@code ../} at the beginning of a relative path. + * @throws URISyntaxException if path is empty or has no parent directory available while resolving <code>../</code> + */ + public static String cleanPathString(String path) throws URISyntaxException { + // Resolve './' before '../' to handle case 'parent/./../a.txt' properly. + int idx = path.length() - 1; + while ( idx >= 1 && ( idx = path.lastIndexOf("./", idx) ) >= 0 ) { + if( 0 < idx && path.charAt(idx-1) == '.' ) { + idx-=2; // skip '../' -> idx upfront + } else { + path = path.substring(0, idx) + path.substring(idx+2); + idx--; // idx upfront + } + } + idx = 0; + while ( ( idx = path.indexOf("../", idx) ) >= 0 ) { + if( 0 == idx ) { + idx += 3; // skip starting '../' + } else { + path = getParentOf(path.substring(0, idx)) + path.substring(idx+3); + idx = 0; + } + } + return path; + } + } }
\ No newline at end of file diff --git a/src/byte_stream.cpp b/src/byte_stream.cpp index 81ca200..320e464 100644 --- a/src/byte_stream.cpp +++ b/src/byte_stream.cpp @@ -223,12 +223,12 @@ size_t ByteInStream_File::read(uint8_t out[], size_t length) NOEXCEPT_BOTAN { if( 0 == length || end_of_data() ) { return 0; } - m_source.read(cast_uint8_ptr_to_char(out), length); + m_source->read(cast_uint8_ptr_to_char(out), length); if( error() ) { DBG_PRINT("ByteInStream_File::read: Error occurred in %s", to_string().c_str()); return 0; } - const size_t got = static_cast<size_t>(m_source.gcount()); + const size_t got = static_cast<size_t>(m_source->gcount()); m_bytes_consumed += got; return got; } @@ -241,37 +241,37 @@ size_t ByteInStream_File::peek(uint8_t out[], size_t length, size_t offset) cons if(offset) { secure_vector<uint8_t> buf(offset); - m_source.read(cast_uint8_ptr_to_char(buf.data()), buf.size()); + m_source->read(cast_uint8_ptr_to_char(buf.data()), buf.size()); if( error() ) { DBG_PRINT("ByteInStream_File::peek: Error occurred (offset) in %s", to_string().c_str()); return 0; } - got = static_cast<size_t>(m_source.gcount()); + got = static_cast<size_t>(m_source->gcount()); } if(got == offset) { - m_source.read(cast_uint8_ptr_to_char(out), length); + m_source->read(cast_uint8_ptr_to_char(out), length); if( error() ) { DBG_PRINT("ByteInStream_File::peek: Error occurred (read) in %s", to_string().c_str()); return 0; } - got = static_cast<size_t>(m_source.gcount()); + got = static_cast<size_t>(m_source->gcount()); } - if(m_source.eof()) { - m_source.clear(); + if(m_source->eof()) { + m_source->clear(); } - m_source.seekg(m_bytes_consumed, std::ios::beg); + m_source->seekg(m_bytes_consumed, std::ios::beg); return got; } bool ByteInStream_File::check_available(size_t n) NOEXCEPT_BOTAN { - return m_content_size - m_bytes_consumed >= (uint64_t)n; + return nullptr != m_source && m_content_size - m_bytes_consumed >= (uint64_t)n; }; bool ByteInStream_File::end_of_data() const NOEXCEPT_BOTAN { - return !m_source.good() || m_bytes_consumed >= m_content_size; + return nullptr == m_source || !m_source->good() || m_bytes_consumed >= m_content_size; } std::string ByteInStream_File::id() const NOEXCEPT_BOTAN { @@ -280,25 +280,35 @@ std::string ByteInStream_File::id() const NOEXCEPT_BOTAN { ByteInStream_File::ByteInStream_File(const std::string& path, bool use_binary) noexcept : m_identifier(path), - m_source(path, use_binary ? std::ios::binary : std::ios::in), + m_source(), m_content_size(0), m_bytes_consumed(0) { - if( error() ) { - DBG_PRINT("ByteInStream_File::ctor: Error occurred in %s", to_string().c_str()); + std::unique_ptr<jau::fs::file_stats> stats; + if( jau::io::uri::is_local_file_protocol(path) ) { + // cut of leading `file://` + std::string path2 = path.substr(7); + stats = std::make_unique<jau::fs::file_stats>(path2); + } else { + stats = std::make_unique<jau::fs::file_stats>(path); + } + if( !stats->exists() || !stats->has_access() ) { + DBG_PRINT("ByteInStream_File::ctor: Error, not an existing or accessible file in %s, %s", stats->to_string(true).c_str(), to_string().c_str()); + } else if( !stats->is_file() ) { + DBG_PRINT("ByteInStream_File::ctor: Error, not a file in %s, %s", stats->to_string(true).c_str(), to_string().c_str()); } else { - const jau::fs::file_stats in_stats(path); - if( !in_stats.exists() || !in_stats.has_access() ) { - DBG_PRINT("ByteInStream_File::ctor: Error, not an existing or accessible file in %s", to_string().c_str()); - } else if( !in_stats.is_file() ) { - DBG_PRINT("ByteInStream_File::ctor: Error, not a file in %s", to_string().c_str()); - } else { - m_content_size = in_stats.size(); + m_content_size = stats->size(); + m_source = std::make_unique<std::ifstream>(stats->path(), use_binary ? std::ios::binary : std::ios::in); + if( error() ) { + DBG_PRINT("ByteInStream_File::ctor: Error occurred in %s, %s", stats->to_string(true), to_string().c_str()); + m_source = nullptr; } } } void ByteInStream_File::close() noexcept { - m_source.close(); + if( nullptr != m_source ) { + m_source->close(); + } } std::string ByteInStream_File::to_string() const noexcept { @@ -310,6 +320,7 @@ std::string ByteInStream_File::to_string() const noexcept { "]"; } + ByteInStream_URL::ByteInStream_URL(const std::string& url, const jau::fraction_i64& timeout) noexcept : m_url(url), m_timeout(timeout), m_buffer(0x00, BEST_URLSTREAM_RINGBUFFER_SIZE), m_has_content_length( false ), m_content_size( 0 ), m_total_xfered( 0 ), m_result( io::async_io_result_t::NONE ), @@ -317,6 +328,10 @@ ByteInStream_URL::ByteInStream_URL(const std::string& url, const jau::fraction_i { m_url_thread = read_url_stream(m_url, m_buffer, m_has_content_length, m_content_size, m_total_xfered, m_result); + if( nullptr == m_url_thread ) { + // url protocol not supported + m_result = async_io_result_t::FAILED; + } } void ByteInStream_URL::close() noexcept { @@ -327,10 +342,11 @@ void ByteInStream_URL::close() noexcept { } m_buffer.drop(m_buffer.size()); // unblock putBlocking(..) - if( m_url_thread.joinable() ) { + if( nullptr != m_url_thread && m_url_thread->joinable() ) { DBG_PRINT("ByteInStream_URL: close.1 %s, %s", id().c_str(), m_buffer.toString().c_str()); - m_url_thread.join(); + m_url_thread->join(); } + m_url_thread = nullptr; DBG_PRINT("ByteInStream_URL: close.X %s, %s", id().c_str(), to_string_int().c_str()); } @@ -384,6 +400,22 @@ std::string ByteInStream_URL::to_string() const noexcept { return "ByteInStream_URL["+to_string_int()+"]"; } +std::unique_ptr<ByteInStream> jau::io::to_ByteInStream(const std::string& path_or_uri, jau::fraction_i64 timeout) noexcept { + if( !jau::io::uri::is_local_file_protocol(path_or_uri) && + jau::io::uri::protocol_supported(path_or_uri) ) + { + std::unique_ptr<ByteInStream> res = std::make_unique<ByteInStream_URL>(path_or_uri, timeout); + if( nullptr != res && !res->error() ) { + return res; + } + } + std::unique_ptr<ByteInStream> res = std::make_unique<ByteInStream_File>(path_or_uri); + if( nullptr != res && !res->error() ) { + return res; + } + return nullptr; +} + ByteInStream_Feed::ByteInStream_Feed(const std::string& id_name, const jau::fraction_i64& timeout) noexcept : m_id(id_name), m_timeout(timeout), m_buffer(0x00, BEST_URLSTREAM_RINGBUFFER_SIZE), m_has_content_length( false ), m_content_size( 0 ), m_total_xfered( 0 ), m_result( io::async_io_result_t::NONE ), diff --git a/src/io_util.cpp b/src/io_util.cpp index 3a65d05..cdede16 100644 --- a/src/io_util.cpp +++ b/src/io_util.cpp @@ -82,6 +82,53 @@ uint64_t jau::io::read_stream(ByteInStream& in, return total; } +std::vector<std::string_view> jau::io::uri::supported_protocols() noexcept { + const curl_version_info_data* cvid = curl_version_info(CURLVERSION_NOW); + if( nullptr == cvid || nullptr == cvid->protocols ) { + return std::vector<std::string_view>(); + } + std::vector<std::string_view> res; + for(int i=0; nullptr != cvid->protocols[i]; ++i) { + res.push_back( std::string_view(cvid->protocols[i]) ); + } + return res; +} + +static bool _is_scheme_valid(const std::string_view& scheme) noexcept { + if ( scheme.empty() ) { + return false; + } + auto pos = std::find_if_not(scheme.begin(), scheme.end(), [&](char c){ + return std::isalnum(c) || c == '+' || c == '.' || c == '-'; + }); + return pos == scheme.end(); +} +std::string_view jau::io::uri::get_scheme(const std::string_view& uri) noexcept { + std::size_t pos = uri.find(':'); + if (pos == std::string_view::npos) { + return uri.substr(0, 0); + } + std::string_view scheme = uri.substr(0, pos); + if( !_is_scheme_valid( scheme ) ) { + return uri.substr(0, 0); + } + return scheme; +} + +bool jau::io::uri::protocol_supported(const std::string_view& uri) noexcept { + const std::string_view scheme = get_scheme(uri); + if( scheme.empty() ) { + return false; + } + const std::vector<std::string_view> protos = supported_protocols(); + auto it = std::find(protos.cbegin(), protos.cend(), scheme); + return protos.cend() != it; +} + +bool jau::io::uri::is_local_file_protocol(const std::string_view& uri) noexcept { + return 0 == uri.find("file://"); +} + struct curl_glue1_t { CURL *curl_handle; bool has_content_length; @@ -178,6 +225,13 @@ uint64_t jau::io::read_url_stream(const std::string& url, errorbuffer.reserve(CURL_ERROR_SIZE); CURLcode res; + if( !uri::protocol_supported(url) ) { + const std::string_view scheme = uri::get_scheme(url); + DBG_PRINT("Protocol of given uri-scheme '%s' not supported. Supported protocols [%s].", + std::string(scheme).c_str(), to_string(uri::supported_protocols(), ",").c_str()); + return 0; + } + /* init the curl session */ CURL *curl_handle = curl_easy_init(); if( nullptr == curl_handle ) { @@ -519,16 +573,24 @@ cleanup: return; } -std::thread jau::io::read_url_stream(const std::string& url, - ByteRingbuffer& buffer, - jau::relaxed_atomic_bool& has_content_length, - jau::relaxed_atomic_uint64& content_length, - jau::relaxed_atomic_uint64& total_read, - relaxed_atomic_async_io_result_t& result) noexcept { +std::unique_ptr<std::thread> jau::io::read_url_stream(const std::string& url, + ByteRingbuffer& buffer, + jau::relaxed_atomic_bool& has_content_length, + jau::relaxed_atomic_uint64& content_length, + jau::relaxed_atomic_uint64& total_read, + relaxed_atomic_async_io_result_t& result) noexcept { /* init user referenced values */ has_content_length = false; content_length = 0; total_read = 0; + + if( !uri::protocol_supported(url) ) { + result = io::async_io_result_t::FAILED; + const std::string_view scheme = uri::get_scheme(url); + DBG_PRINT("Protocol of given uri-scheme '%s' not supported. Supported protocols [%s].", + std::string(scheme).c_str(), to_string(uri::supported_protocols(), ",").c_str()); + return nullptr; + } result = io::async_io_result_t::NONE; if( buffer.capacity() < BEST_URLSTREAM_RINGBUFFER_SIZE ) { @@ -537,7 +599,7 @@ std::thread jau::io::read_url_stream(const std::string& url, std::unique_ptr<curl_glue2_t> cg ( std::make_unique<curl_glue2_t>(nullptr, has_content_length, content_length, total_read, buffer, result ) ); - return std::thread(&::read_url_stream_thread, url.c_str(), std::move(cg)); // @suppress("Invalid arguments") + return std::make_unique<std::thread>(&::read_url_stream_thread, url.c_str(), std::move(cg)); // @suppress("Invalid arguments") } void jau::io::print_stats(const std::string& prefix, const uint64_t& out_bytes_total, const jau::fraction_i64& td) noexcept { diff --git a/test/test_bytestream01.cpp b/test/test_bytestream01.cpp index 46435e8..63cd1fb 100644 --- a/test/test_bytestream01.cpp +++ b/test/test_bytestream01.cpp @@ -80,8 +80,8 @@ class TestByteStream01 { fname_payload_size_lst.push_back( size ); } data() { - add_test_file("test_cipher_01_11kiB", 1024*11); - add_test_file("test_cipher_02_65MiB", 1024*1024*65); + add_test_file("testfile_blob_01_11kiB.bin", 1024*11); + add_test_file("testfile_blob_02_65MiB.bin", 1024*1024*65); } public: static const data& get() { @@ -99,13 +99,14 @@ class TestByteStream01 { ~TestByteStream01() { std::system("killall mini_httpd"); - std::system("killall mini_httpd"); } static void httpd_start() { std::system("killall mini_httpd"); - std::system("killall mini_httpd"); - std::system("/usr/sbin/mini_httpd -p 8080"); + const std::string cwd = jau::fs::get_cwd(); + const std::string cmd = "/usr/sbin/mini_httpd -p 8080 -l "+cwd+"/mini_httpd.log"; + jau::PLAIN_PRINT(true, "%s", cmd.c_str()); + std::system(cmd.c_str()); } static bool transfer(jau::io::ByteInStream& input, const std::string& output_fname) { @@ -160,6 +161,130 @@ class TestByteStream01 { return true; } + void test00a_protocols_error() { + httpd_start(); + { + std::vector<std::string_view> protos = jau::io::uri::supported_protocols(); + jau::PLAIN_PRINT(true, "test00_protocols: Supported protocols: %zu: %s", protos.size(), jau::to_string(protos, ",").c_str()); + REQUIRE( 0 < protos.size() ); + } + const size_t file_idx = IDX_11kiB; + { + const std::string url = "not_exiting_file.txt"; + REQUIRE( false == jau::io::uri::is_local_file_protocol(url) ); + REQUIRE( false == jau::io::uri::protocol_supported(url) ); + + std::unique_ptr<jau::io::ByteInStream> in = jau::io::to_ByteInStream(url); + if( nullptr != in ) { + jau::PLAIN_PRINT(true, "test00_protocols: not_exiting_file: %s", in->to_string().c_str()); + } + REQUIRE( nullptr == in ); + } + { + const std::string url = "file://not_exiting_file_uri.txt"; + REQUIRE( true == jau::io::uri::is_local_file_protocol(url) ); + REQUIRE( true == jau::io::uri::protocol_supported(url) ); + + std::unique_ptr<jau::io::ByteInStream> in = jau::io::to_ByteInStream(url); + if( nullptr != in ) { + jau::PLAIN_PRINT(true, "test00_protocols: not_exiting_file_uri: %s", in->to_string().c_str()); + } + REQUIRE( nullptr == in ); + } + { + const std::string url = "lala://localhost:8080/" + fname_payload_lst[file_idx]; + REQUIRE( false == jau::io::uri::is_local_file_protocol(url) ); + REQUIRE( false == jau::io::uri::protocol_supported(url) ); + + std::unique_ptr<jau::io::ByteInStream> in = jau::io::to_ByteInStream(url); + if( nullptr != in ) { + jau::PLAIN_PRINT(true, "test00_protocols: not_exiting_protocol_uri: %s", in->to_string().c_str()); + } + REQUIRE( nullptr == in ); + } + { + const std::string url = url_input_root + "not_exiting_http_uri.txt"; + REQUIRE( false == jau::io::uri::is_local_file_protocol(url) ); + REQUIRE( true == jau::io::uri::protocol_supported(url) ); + + std::unique_ptr<jau::io::ByteInStream> in = jau::io::to_ByteInStream(url); + REQUIRE( nullptr != in ); + jau::sleep_for( 10_ms ); + jau::PLAIN_PRINT(true, "test00_protocols: not_exiting_http_uri: %s", in->to_string().c_str()); + REQUIRE( true == in->end_of_data() ); + REQUIRE( true == in->error() ); + REQUIRE( 0 == in->content_size() ); + } + } + + void test00b_protocols_ok() { + httpd_start(); + const size_t file_idx = IDX_11kiB; + { + const std::string url = fname_payload_lst[file_idx]; + REQUIRE( false == jau::io::uri::is_local_file_protocol(url) ); + REQUIRE( false == jau::io::uri::protocol_supported(url) ); + + std::unique_ptr<jau::io::ByteInStream> in = jau::io::to_ByteInStream(url); + if( nullptr != in ) { + jau::PLAIN_PRINT(true, "test00_protocols: local-file-0: %s", in->to_string().c_str()); + } + REQUIRE( nullptr != in ); + REQUIRE( false == in->error() ); + + bool res = transfer(*in, fname_payload_copy_lst[file_idx]); + REQUIRE( true == res ); + + jau::fs::file_stats out_stats(fname_payload_copy_lst[file_idx]); + REQUIRE( true == out_stats.exists() ); + REQUIRE( true == out_stats.is_file() ); + REQUIRE( in->content_size() == out_stats.size() ); + REQUIRE( fname_payload_size_lst[file_idx] == out_stats.size() ); + } + { + const std::string url = "file://" + fname_payload_lst[file_idx]; + REQUIRE( true == jau::io::uri::is_local_file_protocol(url) ); + REQUIRE( true == jau::io::uri::protocol_supported(url) ); + + std::unique_ptr<jau::io::ByteInStream> in = jau::io::to_ByteInStream(url); + if( nullptr != in ) { + jau::PLAIN_PRINT(true, "test00_protocols: local-file-1: %s", in->to_string().c_str()); + } + REQUIRE( nullptr != in ); + REQUIRE( false == in->error() ); + + bool res = transfer(*in, fname_payload_copy_lst[file_idx]); + REQUIRE( true == res ); + + jau::fs::file_stats out_stats(fname_payload_copy_lst[file_idx]); + REQUIRE( true == out_stats.exists() ); + REQUIRE( true == out_stats.is_file() ); + REQUIRE( in->content_size() == out_stats.size() ); + REQUIRE( fname_payload_size_lst[file_idx] == out_stats.size() ); + } + { + const std::string url = url_input_root + fname_payload_lst[file_idx]; + REQUIRE( false == jau::io::uri::is_local_file_protocol(url) ); + REQUIRE( true == jau::io::uri::protocol_supported(url) ); + + std::unique_ptr<jau::io::ByteInStream> in = jau::io::to_ByteInStream(url); + if( nullptr != in ) { + jau::PLAIN_PRINT(true, "test00_protocols: http: %s", in->to_string().c_str()); + } + REQUIRE( nullptr != in ); + REQUIRE( false == in->error() ); + + bool res = transfer(*in, fname_payload_copy_lst[file_idx]); + REQUIRE( true == res ); + + jau::fs::file_stats out_stats(fname_payload_copy_lst[file_idx]); + REQUIRE( true == out_stats.exists() ); + REQUIRE( true == out_stats.is_file() ); + REQUIRE( in->content_size() == out_stats.size() ); + REQUIRE( fname_payload_size_lst[file_idx] == out_stats.size() ); + } + } + void test01_copy_file_ok() { { const size_t file_idx = IDX_11kiB; @@ -475,12 +600,13 @@ std::vector<std::string> TestByteStream01::fname_payload_lst; std::vector<std::string> TestByteStream01::fname_payload_copy_lst; std::vector<uint64_t> TestByteStream01::fname_payload_size_lst; -METHOD_AS_TEST_CASE( TestByteStream01::test01_copy_file_ok, "TestByteStream01 test01_copy_file_ok"); - -METHOD_AS_TEST_CASE( TestByteStream01::test11_copy_http_ok, "TestByteStream01 test11_copy_http_ok"); -METHOD_AS_TEST_CASE( TestByteStream01::test12_copy_http_404, "TestByteStream01 test12_copy_http_404"); +METHOD_AS_TEST_CASE( TestByteStream01::test00a_protocols_error, "TestByteStream01 test00a_protocols_error"); +METHOD_AS_TEST_CASE( TestByteStream01::test00b_protocols_ok, "TestByteStream01 test00b_protocols_ok"); -METHOD_AS_TEST_CASE( TestByteStream01::test21_copy_fed_ok, "TestByteStream01 test21_copy_fed_ok"); -METHOD_AS_TEST_CASE( TestByteStream01::test22_copy_fed_irq, "TestByteStream01 test22_copy_fed_irq"); +METHOD_AS_TEST_CASE( TestByteStream01::test01_copy_file_ok, "TestByteStream01 test01_copy_file_ok"); +METHOD_AS_TEST_CASE( TestByteStream01::test11_copy_http_ok, "TestByteStream01 test11_copy_http_ok"); +METHOD_AS_TEST_CASE( TestByteStream01::test12_copy_http_404, "TestByteStream01 test12_copy_http_404"); +METHOD_AS_TEST_CASE( TestByteStream01::test21_copy_fed_ok, "TestByteStream01 test21_copy_fed_ok"); +METHOD_AS_TEST_CASE( TestByteStream01::test22_copy_fed_irq, "TestByteStream01 test22_copy_fed_irq"); diff --git a/test/test_iostream01.cpp b/test/test_iostream01.cpp index d82f7d2..d1809ff 100644 --- a/test/test_iostream01.cpp +++ b/test/test_iostream01.cpp @@ -53,7 +53,7 @@ using namespace jau::fractions_i64_literals; class TestIOStream01 { public: const std::string url_input_root = "http://localhost:8080/"; - const std::string basename_10kiB = "data-10kiB.bin"; + const std::string basename_10kiB = "testfile_data_10kiB.bin"; TestIOStream01() { // produce fresh demo data @@ -71,13 +71,76 @@ class TestIOStream01 { } } std::system("killall mini_httpd"); - std::system("killall mini_httpd"); - std::system("/usr/sbin/mini_httpd -p 8080"); + const std::string cwd = jau::fs::get_cwd(); + const std::string cmd = "/usr/sbin/mini_httpd -p 8080 -l "+cwd+"/mini_httpd.log"; + jau::PLAIN_PRINT(true, "%s", cmd.c_str()); + std::system(cmd.c_str()); } ~TestIOStream01() { std::system("killall mini_httpd"); - std::system("killall mini_httpd"); + } + + void test00_protocols() { + { + std::vector<std::string_view> protos = jau::io::uri::supported_protocols(); + jau::PLAIN_PRINT(true, "test00_protocols: Supported protocols: %zu: %s", protos.size(), jau::to_string(protos, ",").c_str()); + REQUIRE( 0 < protos.size() ); + } + { + const std::string url = url_input_root + basename_10kiB; + REQUIRE( false == jau::io::uri::is_local_file_protocol(url) ); + REQUIRE( true == jau::io::uri::protocol_supported(url) ); + } + { + const std::string url = "https://localhost:8080/" + basename_10kiB; + REQUIRE( false == jau::io::uri::is_local_file_protocol(url) ); + REQUIRE( true == jau::io::uri::protocol_supported(url) ); + } + { + const std::string url = "file://" + basename_10kiB; + REQUIRE( true == jau::io::uri::is_local_file_protocol(url) ); + REQUIRE( true == jau::io::uri::protocol_supported(url) ); + } + { + const std::string url = "lala://localhost:8080/" + basename_10kiB; + REQUIRE( false == jau::io::uri::is_local_file_protocol(url) ); + REQUIRE( false == jau::io::uri::protocol_supported(url) ); + } + { + // sync read_url_stream w/ unknown protocol + const std::string url = "lala://localhost:8080/" + basename_10kiB; + jau::io::secure_vector<uint8_t> buffer(4096); + size_t consumed_calls = 0; + uint64_t consumed_total_bytes = 0; + jau::io::StreamConsumerFunc consume = [&](jau::io::secure_vector<uint8_t>& data, bool is_final) noexcept -> bool { + (void)is_final; + consumed_calls++; + consumed_total_bytes += data.size(); + return true; + }; + uint64_t http_total_bytes = jau::io::read_url_stream(url, buffer, consume); + REQUIRE( 0 == http_total_bytes ); + REQUIRE( consumed_total_bytes == http_total_bytes ); + REQUIRE( 0 == consumed_calls ); + } + { + // async read_url_stream w/ unknown protocol + const std::string url = "lala://localhost:8080/" + basename_10kiB; + + jau::io::ByteRingbuffer rb(0x00, jau::io::BEST_URLSTREAM_RINGBUFFER_SIZE); + jau::relaxed_atomic_bool url_has_content_length; + jau::relaxed_atomic_uint64 url_content_length; + jau::relaxed_atomic_uint64 url_total_read; + jau::io::relaxed_atomic_async_io_result_t result; + + std::unique_ptr<std::thread> http_thread = jau::io::read_url_stream(url, rb, url_has_content_length, url_content_length, url_total_read, result); + REQUIRE( nullptr == http_thread ); + REQUIRE( url_has_content_length == false ); + REQUIRE( url_content_length == 0 ); + REQUIRE( url_content_length == url_total_read ); + REQUIRE( jau::io::async_io_result_t::FAILED == result ); + } } void test01_sync_ok() { @@ -85,7 +148,7 @@ class TestIOStream01 { const size_t file_size = in_stats.size(); const std::string url_input = url_input_root + basename_10kiB; - std::ofstream outfile("test01_01_out.bin", std::ios::out | std::ios::binary); + std::ofstream outfile("testfile01_01_out.bin", std::ios::out | std::ios::binary); REQUIRE( outfile.good() ); REQUIRE( outfile.is_open() ); @@ -112,7 +175,7 @@ class TestIOStream01 { void test02_sync_404() { const std::string url_input = url_input_root + "doesnt_exists.txt"; - std::ofstream outfile("test02_01_out.bin", std::ios::out | std::ios::binary); + std::ofstream outfile("testfile02_01_out.bin", std::ios::out | std::ios::binary); REQUIRE( outfile.good() ); REQUIRE( outfile.is_open() ); @@ -141,7 +204,7 @@ class TestIOStream01 { const size_t file_size = in_stats.size(); const std::string url_input = url_input_root + basename_10kiB; - std::ofstream outfile("test11_01_out.bin", std::ios::out | std::ios::binary); + std::ofstream outfile("testfile11_01_out.bin", std::ios::out | std::ios::binary); REQUIRE( outfile.good() ); REQUIRE( outfile.is_open() ); @@ -152,7 +215,8 @@ class TestIOStream01 { jau::relaxed_atomic_uint64 url_total_read; jau::io::relaxed_atomic_async_io_result_t result; - std::thread http_thread = jau::io::read_url_stream(url_input, rb, url_has_content_length, url_content_length, url_total_read, result); + std::unique_ptr<std::thread> http_thread = jau::io::read_url_stream(url_input, rb, url_has_content_length, url_content_length, url_total_read, result); + REQUIRE( nullptr != http_thread ); jau::io::secure_vector<uint8_t> buffer(buffer_size); size_t consumed_loops = 0; @@ -171,7 +235,7 @@ class TestIOStream01 { jau::PLAIN_PRINT(true, "test11_async_ok.X Done: total %" PRIu64 ", result %d, rb %s", consumed_total_bytes, (int)result.load(), rb.toString().c_str() ); - http_thread.join(); + http_thread->join(); REQUIRE( url_has_content_length == true ); REQUIRE( url_content_length == file_size ); @@ -184,7 +248,7 @@ class TestIOStream01 { void test12_async_404() { const std::string url_input = url_input_root + "doesnt_exists.txt"; - std::ofstream outfile("test12_01_out.bin", std::ios::out | std::ios::binary); + std::ofstream outfile("testfile12_01_out.bin", std::ios::out | std::ios::binary); REQUIRE( outfile.good() ); REQUIRE( outfile.is_open() ); @@ -195,7 +259,8 @@ class TestIOStream01 { jau::relaxed_atomic_uint64 url_total_read; jau::io::relaxed_atomic_async_io_result_t result; - std::thread http_thread = jau::io::read_url_stream(url_input, rb, url_has_content_length, url_content_length, url_total_read, result); + std::unique_ptr<std::thread> http_thread = jau::io::read_url_stream(url_input, rb, url_has_content_length, url_content_length, url_total_read, result); + REQUIRE( nullptr != http_thread ); jau::io::secure_vector<uint8_t> buffer(buffer_size); size_t consumed_loops = 0; @@ -214,7 +279,7 @@ class TestIOStream01 { jau::PLAIN_PRINT(true, "test12_async_404.X Done: total %" PRIu64 ", result %d, rb %s", consumed_total_bytes, (int)result.load(), rb.toString().c_str() ); - http_thread.join(); + http_thread->join(); REQUIRE( url_has_content_length == false ); REQUIRE( url_content_length == 0 ); @@ -226,6 +291,7 @@ class TestIOStream01 { }; +METHOD_AS_TEST_CASE( TestIOStream01::test00_protocols, "TestIOStream01 - test00_protocols"); METHOD_AS_TEST_CASE( TestIOStream01::test01_sync_ok, "TestIOStream01 - test01_sync_ok"); METHOD_AS_TEST_CASE( TestIOStream01::test02_sync_404, "TestIOStream01 - test02_sync_404"); METHOD_AS_TEST_CASE( TestIOStream01::test11_async_ok, "TestIOStream01 - test11_async_ok"); |