/** \file * IStream adapters. */ /* * Copyright (C) 2013 Alexander Lamaison * * This material is provided "as is", with absolutely no warranty * expressed or implied. Any use is at your own risk. Permission to * use or copy this software for any purpose is hereby granted without * fee, provided the above notices are retained on all copies. * Permission to modify the code and to distribute modified code is * granted, provided the above notices are retained, and a notice that * the code was modified is included with the above copyright notice. * * This header is part of Comet version 2. * https://github.com/alamaison/comet */ #ifndef COMET_STREAM_H #define COMET_STREAM_H #include #include // bstr_t #include // com_error #include // COMET_CATCH_CLASS_INTERFACE_BOUNDARY #include // simple_object #include // assert #include #include // numeric_limits #include #include // StringCbCopyW namespace comet { namespace impl { static const size_t COPY_CHUNK_SIZE = 512; /** * Used to clear stream failure on exiting a scope. * * Useful when trying an operation and converting any failure to an * exception. In that case there is no need for the stream object to * also retain evidence of the failure. * * Note, only clears `failbit`. `eofbit` is left alone as it is a * valid consequence of operations performed by the wrapper. `badbit` is * also left as these errors are unrecoverable and therefore the * stream is as good as dead; a caller of the unwrapped stream needs * to know that. */ template class stream_failure_cleanser { public: explicit stream_failure_cleanser(Stream& stream) : m_stream(stream) {} ~stream_failure_cleanser() throw() { try { m_stream.clear(m_stream.rdstate() & ~std::ios_base::failbit); } catch (const std::exception&) {} } private: stream_failure_cleanser(const stream_failure_cleanser&); stream_failure_cleanser& operator=(const stream_failure_cleanser&); Stream& m_stream; }; // These do_read/write helper functions dispatch the stream-type specific code // so that we only need one class to implement all the stream adapters inline void do_istream_read( std::istream& stream, void* buffer, ULONG buffer_size_in_bytes, ULONG& bytes_read_out) { typedef std::istream stream_type; // // IMPORTANT: The bytes_read_out count must be correct even in the // error case, EVEN if that error is an exception, because the caller // can treat an error as EOF (see Read method docs). // // However, in the error case it is acceptable to make the read count // smaller than the actual number of bytes read (e.g. 0) because the // caller is losing the unread part of the stream anyway. Losing a few // extra bytes that are already in the buffer but not declared valid // by the byte count will not make the situation worse. bytes_read_out = 0U; stream.read( reinterpret_cast(buffer), buffer_size_in_bytes / sizeof(std::istream::char_type)); // Any failure not caused by eof is a failure. // Badbit is always a failure because, even if we are at eof, the stream // encountered a problem that needs reporting. if (stream.bad() || (stream.fail() && !stream.eof())) { throw std::runtime_error("Reading from stream failed"); } else { if (stream.gcount() >= 0 && // temp conversion to unsigned larger than ULONG to catch // values larger than ULONG static_cast(stream.gcount()) < (std::numeric_limits::max)() / sizeof(std::istream::char_type)) { bytes_read_out = static_cast( stream.gcount() * sizeof(std::istream::char_type)); } assert(stream.eof() || bytes_read_out == buffer_size_in_bytes); } } inline void do_ostream_write( std::ostream& stream, const void* buffer, ULONG buffer_size_in_bytes, ULONG& bytes_written_out) { // If there is an error we cannot get a reliable write count, even // if we were to use stream.rdbuf()->sputn, because the failure // might well happen during the flush operation which doesn't let // us find out how much was flushed before failing. typedef std::ostream stream_type; bytes_written_out = 0U; stream.write( reinterpret_cast(buffer), buffer_size_in_bytes / sizeof(std::ostream::char_type)); try { stream.flush(); } catch (const std::exception& e) { throw com_error(e.what(), STG_E_MEDIUMFULL); } if (stream.good()) { bytes_written_out = buffer_size_in_bytes; } else { throw com_error("Writing to stream failed", STG_E_MEDIUMFULL); } } /** Ensure read position updated even in case of exception */ template class read_position_finaliser { public: read_position_finaliser( Stream& stream, std::streampos& new_position_out) : m_stream(stream), m_new_position_out(new_position_out) {} ~read_position_finaliser() throw() { try { // we still want the read position if previous op failed m_stream.clear(); std::streampos new_position = m_stream.tellg(); if (m_stream) { m_new_position_out = new_position; } else { m_new_position_out = std::streampos(); } } catch (...) { // We tried. Nothing else we can do // // We likely ended up here in the exception unwinding of a // failed seek because a non-seekable stream chose to let us // know by throwing an exception from the streambuf // (e.g. Boost.IOStream). Non-seekable is also non-tellable so // telgp does the same thing. // // We have prevent this exception propagating else we get an // thrown while unwinding another exception causing terminate() m_new_position_out = std::streampos(-1); } } private: read_position_finaliser(const read_position_finaliser&); read_position_finaliser& operator=(const read_position_finaliser&); Stream& m_stream; std::streampos& m_new_position_out; }; /** Ensure write position updated even in case of exception */ template class write_position_finaliser { public: write_position_finaliser( Stream& stream, std::streampos& new_position_out) : m_stream(stream), m_new_position_out(new_position_out) {} ~write_position_finaliser() throw() { try { // we still want the write position if previous op failed m_stream.clear(); std::streampos new_position = m_stream.tellp(); if (m_stream) { m_new_position_out = new_position; } else { m_new_position_out = std::streampos(); } } catch (...) { // We tried. Nothing else we can do // // We likely ended up here in the exception unwinding of a // failed seek because a non-seekable stream chose to let us // know by throwing an exception from the streambuf // (e.g. Boost.IOStream). Non-seekable is also non-tellable so // tellp does the same thing. // // We have prevent this exception propagating else we get an // thrown while unwinding another exception causing terminate() m_new_position_out = std::streampos(-1); } } private: write_position_finaliser(const write_position_finaliser&); write_position_finaliser& operator=(const write_position_finaliser&); Stream& m_stream; std::streampos& m_new_position_out; }; /** * Copies `streampos` out-parameter to ULARGE_INTEGER out parameter * in the face of exceptions. * * Using this class means you can concentrate on keeping the `streampos` * variable updated, confident in the knowledge the the COM-interface * ULARGE_INTEGER parameter will eventually be updated to match it, * no matter what. * * Assumes that the source out parameter is guaranteed to be valid at * all times. Valid doesn't necessarily mean correct but that it always * contains the value we want to set the out parameter to, even if that is * wrong (for instance failure while calculating correct position). */ class position_out_converter { public: position_out_converter( std::streampos& source_out_parameter, ULARGE_INTEGER* destination_out_parameter) : m_source(source_out_parameter), m_destination(destination_out_parameter) {} ~position_out_converter() { if (m_destination) { try { // Convert to streamoff because streampos may not // be an integer. streamoff is guaranteed to be. std::streamoff offset_from_beginning = m_source; if (offset_from_beginning < 0) { // Invalid offset, for example seeking not supported // The error itself is dealt with right after seeking // so this class just has to convert it to something // reasonable for an unsigned value m_destination->QuadPart = 0U; } else { m_destination->QuadPart = offset_from_beginning; } } catch(const std::exception&) { // Only way this can happen is if streampos refuses to // convert to streamoff in which case we really are screwed. m_destination->QuadPart = 0U; } } } private: position_out_converter(const position_out_converter&); position_out_converter& operator=(const position_out_converter&); std::streampos& m_source; ULARGE_INTEGER* m_destination; }; /** * Increments a total counter with an increment are end of scope, * regardless of how that scope is ended. * * Assumes the increment parameter is valid at all times. */ class byte_count_incrementer { public: byte_count_incrementer( ULARGE_INTEGER* total, ULONG& increment) : m_total(total), m_increment(increment) {} ~byte_count_incrementer() { if (m_total) { m_total->QuadPart += m_increment; } } private: byte_count_incrementer(const byte_count_incrementer&); byte_count_incrementer& operator=(const byte_count_incrementer&); ULARGE_INTEGER* m_total; ULONG& m_increment; }; template class position_resetter { public: position_resetter(StreamTraits& traits, std::streampos position) : m_traits(traits), m_position(position) {} ~position_resetter() { try { std::streampos new_position; m_traits.do_seek( m_position, std::ios_base::beg, new_position); assert(new_position == m_position); } catch (const std::exception&) {} } private: position_resetter(const position_resetter&); position_resetter& operator=(const position_resetter&); StreamTraits& m_traits; std::streampos m_position; }; /** * Calculate offset of end of stream from start. */ template inline std::streamoff stream_size(StreamTraits& traits) { position_resetter(traits, traits.do_tell()); std::streampos new_position; traits.do_seek(0, std::ios_base::end, new_position); return new_position; } template class stream_traits; template class stream_traits { public: explicit stream_traits(Stream& stream) : m_stream(stream) {} void do_read( void* buffer, ULONG buffer_size_in_bytes, ULONG& bytes_read_out) { do_istream_read( m_stream, buffer, buffer_size_in_bytes, bytes_read_out); } void do_write( const void* /*buffer*/, ULONG /*buffer_size_in_bytes*/, ULONG& bytes_written_out) { bytes_written_out = 0U; throw com_error( "std::istream does not support writing", STG_E_ACCESSDENIED); } void do_seek( std::streamoff offset, std::ios_base::seekdir way, std::streampos& new_position_out) { read_position_finaliser position_out_updater( m_stream, new_position_out); if (!m_stream.seekg(offset, way)) { throw std::runtime_error("Unable to change read position"); } } std::streampos do_tell() { std::streampos pos = m_stream.tellg(); if (!m_stream) { throw std::runtime_error("Stream position unavailable"); } return pos; } void do_flush() { throw com_error( "std::istream does not support committing data", STG_E_ACCESSDENIED); } private: stream_traits(const stream_traits&); stream_traits& operator=(const stream_traits&); Stream& m_stream; }; template class stream_traits { public: explicit stream_traits(Stream& stream) : m_stream(stream) {} void do_read( void* /*buffer*/, ULONG /*buffer_size_in_bytes*/, ULONG& bytes_read_out) { bytes_read_out = 0U; throw com_error( "std::ostream does not support reading", STG_E_ACCESSDENIED); } void do_write( const void* buffer, ULONG buffer_size_in_bytes, ULONG& bytes_written_out) { do_ostream_write( m_stream, buffer, buffer_size_in_bytes, bytes_written_out); } void do_seek( std::streamoff offset, std::ios_base::seekdir way, std::streampos& new_position_out) { write_position_finaliser position_out_updater( m_stream, new_position_out); if (!m_stream.seekp(offset, way)) { throw std::runtime_error("Unable to change write position"); } } std::streampos do_tell() { std::streampos pos = m_stream.tellp(); if (!m_stream) { throw std::runtime_error("Stream position unavailable"); } return pos; } void do_flush() { if (!m_stream.flush()) { throw std::runtime_error( "Unable to flush buffer to output sequence"); } } private: stream_traits(const stream_traits&); stream_traits& operator=(const stream_traits&); Stream& m_stream; }; template class stream_traits { private: enum last_stream_operation { read, write }; public: explicit stream_traits(Stream& stream) : m_stream(stream), m_last_op(read) {} void do_read( void* buffer, ULONG buffer_size_in_bytes, ULONG& bytes_read_out) { bytes_read_out = 0U; // sync reading position with writing position, which was the last // one used and is allowed to be different in C++ streams but // not COM IStreams if (m_last_op == write) { m_stream.seekg(m_stream.tellp()); // We ignore errors syncing the positions as even iostreams may // not be seekable at all m_last_op = read; } assert(m_last_op == read); do_istream_read( m_stream, buffer, buffer_size_in_bytes, bytes_read_out); } void do_write( const void* buffer, ULONG buffer_size_in_bytes, ULONG& bytes_written_out) { bytes_written_out = 0U; // sync writing position with reading position, which was the last // one used and is allowed to be different in C++ streams but // not COM IStreams if (m_last_op == read) { m_stream.seekp(m_stream.tellg()); // We ignore errors syncing the positions as even iostreams may // not be seekable at all m_last_op = write; } assert(m_last_op == write); do_ostream_write( m_stream, buffer, buffer_size_in_bytes, bytes_written_out); } void do_seek( std::streamoff offset, std::ios_base::seekdir way, std::streampos& new_position_out) { // Unlike with do_read/do_write, we do not ignore errors when // trying to sync the read/write positions as, if the // first seek succeeded, we know the stream supports // seeking. Therefore a later error is really an error. if (m_last_op == read) { read_position_finaliser position_out_updater( m_stream, new_position_out); if (!m_stream.seekg(offset, way)) { throw std::runtime_error("Unable to change read position"); } else { if (!m_stream.seekp(m_stream.tellg())) { throw std::runtime_error( "Unable to synchronise write position"); } } } else { write_position_finaliser position_out_updater( m_stream, new_position_out); if (!m_stream.seekp(offset, way)) { throw std::runtime_error("Unable to change write position"); } else { if (!m_stream.seekg(m_stream.tellp())) { throw std::runtime_error( "Unable to synchronise read position"); } } } } std::streampos do_tell() { std::streampos pos; if (m_last_op == read) { pos = m_stream.tellg(); } else { pos = m_stream.tellp(); } if (!m_stream) { throw std::runtime_error("Stream position unavailable"); } return pos; } void do_flush() { if (!m_stream.flush()) { throw std::runtime_error( "Unable to flush buffer to output sequence"); } } private: stream_traits(const stream_traits&); stream_traits& operator=(const stream_traits&); Stream& m_stream; last_stream_operation m_last_op; }; /** * Wrap COM IStream interface around C++ IOStream. * * Unlike C++ streams which may have separate read and write positions that * move independently, COM IStreams assume a single combined read/write head. * Therefore this wrapper always starts the next read or write operation * from the where the last operation finished, regardless of whether that * operation was a call to `Read` or `Write`. * * @note This only applies for as long as the read/write positions are * modified only via this wrapper. If the positions are modified by * directly on the underlying IOStream, it is undefined whether the * starting point for the next call to `Read`/`Write` is syncronised * with the end of the previous operation. * * If operations on the inner stream results in failure (the `failbit` * is set), this is communicated via the COM-interface return code. The * `failbit` is cleared before the call returns. This allows further * wrapper methods to be called without having the clear the bit directly * on the underlying stream. Fatal errors (`badbit`) and end-of-file * (`eofbit`) are left unchanged and remain visible in the underlying * stream. */ template class adapted_stream : public simple_object { private: typedef impl::stream_traits< Stream, type_traits::super_sub_class::result, type_traits::super_sub_class::result > stream_traits_type; public: adapted_stream(Stream& stream, const bstr_t& optional_name) : m_stream(stream), m_traits(stream), m_optional_name(optional_name) {} /** * Fill the given buffer with data read from the wrapped C++ stream. * * @param [in] buffer * Destination of bytes to be read. * * @param [in] buffer_size * Size of `buffer` and, therefore, the maximum number of bytes * this method will read. The method may read fewer than this * number of bytes if the request cannot be fulfilled * (e.g. end-of-file, error), in which case only the portion of * `buffer` up to `*read_count_out` bytes from the start * may be considered valid. * * @param [out] read_count_out * Number of bytes actually read into `buffer`. In other words, * the extent of the valid bytes in `buffer` once this method * returns. This value is correct whether the method returns * success or an error code. * * @returns * Error code indicating the success or failure of the method. * @retval `S_OK` * If `buffer` was completely filled. * @retval `S_FALSE` if EOF reached before `buffer` was filled. * `*read_count_out` gives the number of bytes that were read. * @retval a COM error code * If there was a read error. * * Error behaviour rationale * ------------------------- * * MSDN says that, if `read_count_out` < `buffer_size`, callers * should treat both error codes and success codes as meaning * the end-of-file was reached. However, doing so means they will be * unable to distinguish EOF from a genuine read error. Therefore, * we make the stronger guarantee that only `S_FALSE` means EOF * while an error code always indicates a read error. * To accommodate callers who follow the documentation on MSDN, * we ensure that the read-count out-variable is correct even if the * wrapped stream encountered an error (`failbit`, `badbit`) or * threw an exception, because the caller may use the byte-count to * decide which bytes of the partial read are valid. * * In the error case, `*read_count_out` may be less than the number * of bytes actually read from the wrapped stream. This is for * performance reasons: unless we read from the wrapped stream one * byte at a time, there is no way to calculate the the exact number * of bytes read if an exception is thrown part way through reading. * Byte-by-byte reading is hopelessly slow for some applications, such * as an unbuffered stream over a network, so our behaviour is a * pragmatic compromise. We believe it is within the (vaguely) * documented behaviour of `IStream` to treat the out-count in this way * for error cases as the count still delimits a valid region of bytes. */ virtual HRESULT STDMETHODCALLTYPE Read( void* buffer, ULONG buffer_size, ULONG* read_count_out) { impl::stream_failure_cleanser state_resetter(m_stream); if (read_count_out) { *read_count_out = 0U; } try { if (!buffer) { throw com_error("No buffer given", STG_E_INVALIDPOINTER); } // We use a dummy read count location if one is not given so // that do_read can keep it updated correctly even if // do_read throws an exception. ULONG dummy_read_count = 0U; if (!read_count_out) { read_count_out = &dummy_read_count; } m_traits.do_read(buffer, buffer_size, *read_count_out); if (*read_count_out < buffer_size) { assert(m_stream.eof()); return S_FALSE; } else { assert(*read_count_out == buffer_size); assert(m_stream.good()); return S_OK; } } COMET_CATCH_CLASS_INTERFACE_BOUNDARY("Read", "adapted_stream"); } /** * Write the given data to the wrapped C++ stream's controlled sequence. * * The wrapped stream is flushed before this method returns to ensure * the data is written to the controlled sequence (and any associated * errors are detected) rather than just to the stream's buffer. * Therefore, best performance is obtained if this method is called with * as much data as possible for the fewest number of flushes. * * @param [in] data * Bytes to write to the controlled sequence. * * @param [in] data_size * Size of `data` and the number of bytes to write. * * @param [out] written_count_out * Number of bytes guaranteed to be written to the controlled * sequence. This will be `data_size` if writing succeeded. If * an error occurred, this may be fewer than the actual number of * bytes written to the sequence. * * @returns * Error code indicating the success or failure of the method. * @retval `S_OK` * If `data` was completely written to the wrapped stream's * controlled sequence. * @retval a COM error code * If there was a write error. * * Error behaviour rationale * ------------------------- * * If writing succeeds, `*written_count_out` equals `data_size`. If * writing encountered an error `*written_count_out` will be set to zero, * even if some bytes were written out. We do this for performance * reasons: unless we write the data one byte at a time to the wrapped * stream _and flush the stream after each byte_, the C++ streams don't * provide a way to count the exact number of bytes written to the * stream's controlled sequence. Byte-by-byte writing is hopelessly * slow for some applications, such as a stream over a network, so our * behaviour is a pragmatic compromise. * We believe it is within the (vaguely) documented behaviour of * `IStream` to treat the out-count in this way for error cases. * It may seem strange that this method even has an out-count if it * was no intended to be meaningful in the error case (the count would * be equal to `data_size` in the success case), however we believe * it was only intended to be meaningful for non-blocking writes to * asynchronous stream (`E_PENDING` would be returned) which this * wrapper is not (see http://bit.ly/1hZGy1L). */ virtual HRESULT STDMETHODCALLTYPE Write( const void* data, ULONG data_size, ULONG* written_count_out) { stream_failure_cleanser state_resetter(m_stream); if (written_count_out) { *written_count_out = 0U; } try { if (!data) { throw com_error("Buffer not given", STG_E_INVALIDPOINTER); } // We use a dummy written count out location if one is not // given so that do_write can keep it updated correctly even if // do_write throws an exception. ULONG dummy_written_count = 0U; if (!written_count_out) { written_count_out = &dummy_written_count; } m_traits.do_write(data, data_size, *written_count_out); if (*written_count_out < data_size) { throw com_error( "Unable to complete write operation", STG_E_MEDIUMFULL); } else { assert(*written_count_out == data_size); return S_OK; } } COMET_CATCH_CLASS_INTERFACE_BOUNDARY("Write", "adapted_stream"); } virtual HRESULT STDMETHODCALLTYPE Seek( LARGE_INTEGER offset, DWORD origin, ULARGE_INTEGER* new_position_out) { stream_failure_cleanser state_resetter(m_stream); std::streampos new_stream_position_out = std::streampos(); impl::position_out_converter out_param_guard( new_stream_position_out, new_position_out); try { std::ios_base::seekdir way; if (origin == STREAM_SEEK_CUR) { way = std::ios_base::cur; } else if (origin == STREAM_SEEK_SET) { way = std::ios_base::beg; } else if (origin == STREAM_SEEK_END) { way = std::ios_base::end; } else { throw com_error( "Unrecognised stream seek origin", STG_E_INVALIDFUNCTION); } if (offset.QuadPart > (std::numeric_limits::max)()) { throw com_error( "Seek offset too large", STG_E_INVALIDFUNCTION); } else if (offset.QuadPart < (std::numeric_limits::min)()) { throw com_error( "Seek offset too small", STG_E_INVALIDFUNCTION); } else { try { m_traits.do_seek( static_cast(offset.QuadPart), way, new_stream_position_out); } // Translate logic_errors (and subtypes) as they // correspond (very roughly) to the kinds of errors // for which IStream::Seek is documented to return // STG_E_INVALIDFUNCTION catch(const std::logic_error& e) { throw com_error(e.what(), STG_E_INVALIDFUNCTION); } return S_OK; } } COMET_CATCH_CLASS_INTERFACE_BOUNDARY("Seek", "adapted_stream"); } /** * Expand stream to given size. * * The will only increase a stream's size if it is smaller than the * given size. If the stream size is already equal or bigger, it * remains unchanged. * * Not all streams support changing size. In particular, `istream`s * do not support this method. */ virtual HRESULT STDMETHODCALLTYPE SetSize(ULARGE_INTEGER new_size) { try { stream_failure_cleanser state_resetter(m_stream); position_resetter resetter( m_traits, m_traits.do_tell()); if (new_size.QuadPart > 0) { ULARGE_INTEGER new_end_position; new_end_position.QuadPart = new_size.QuadPart - 1; std::streamoff new_offset; if ((std::numeric_limits::max)() < 0) { assert(!"Purely negative number!"); throw com_error( "Seek offset too large", STG_E_INVALIDFUNCTION); } else if (new_end_position.QuadPart > static_cast( (std::numeric_limits::max)())) { throw com_error( "Seek offset too large", STG_E_INVALIDFUNCTION); } else { new_offset = static_cast( new_end_position.QuadPart); } std::streamoff existing_end = stream_size(m_traits); if (new_offset > existing_end) { std::streampos new_position; m_traits.do_seek( new_offset, std::ios_base::beg, new_position); assert(std::streamoff(new_position) == new_offset); // Force the stream to expand by writing NUL at // new extent ULONG bytes_written; m_traits.do_write("\0", 1, bytes_written); assert(bytes_written == 1); } } return S_OK; } COMET_CATCH_CLASS_INTERFACE_BOUNDARY("SetSize", "adapted_stream"); } virtual HRESULT STDMETHODCALLTYPE CopyTo( IStream* destination, ULARGE_INTEGER amount, ULARGE_INTEGER* bytes_read_out, ULARGE_INTEGER* bytes_written_out) { stream_failure_cleanser state_resetter(m_stream); ULARGE_INTEGER dummy_read_out; if (!bytes_read_out) { bytes_read_out = &dummy_read_out; } bytes_read_out->QuadPart = 0U; ULARGE_INTEGER dummy_written_out; if (!bytes_written_out) { bytes_written_out = &dummy_written_out; } bytes_written_out->QuadPart = 0U; try { if (!destination) { throw com_error( "Destination stream not given", STG_E_INVALIDPOINTER); } std::vector buffer(COPY_CHUNK_SIZE); // Perform copy operation in chunks COPY_CHUNK bytes big // The chunk must be less than the biggest ULONG in size // because of the limits of the Read/Write API. Of course // it will be in any case as it would be insane to use more // memory than that, but we make sure anyway using the first // min comparison do { ULONG next_chunk_size = static_cast( min( (std::numeric_limits::max)(), min( amount.QuadPart - bytes_read_out->QuadPart, buffer.size()))); ULONG read_this_round = 0U; ULONG written_this_round = 0U; // These two take care of updating the total on each pass // round the loop as well as on termination, exception or // natural. // // The means the out counts are valid even in the failure // case. MSDN says they don't have to be but, as we can, // we might as well impl::byte_count_incrementer read_incrementer( bytes_read_out, read_this_round); impl::byte_count_incrementer write_incrementer( bytes_written_out, written_this_round); m_traits.do_read( &buffer[0], next_chunk_size, read_this_round); HRESULT hr = destination->Write( &buffer[0], read_this_round, &written_this_round); if (FAILED(hr)) { com_error_from_interface(destination, hr); } if (read_this_round < next_chunk_size) { // EOF break; } } while (amount.QuadPart > bytes_read_out->QuadPart); return S_OK; } COMET_CATCH_CLASS_INTERFACE_BOUNDARY("CopyTo", "adapted_stream"); } /** * Flush data in the buffer to the controlled output sequence. * * This implementation doesn't support transactions so ignores * the commit flags. * * Fails if called with an istream. */ virtual HRESULT STDMETHODCALLTYPE Commit(DWORD /*commit_flags*/) { stream_failure_cleanser state_resetter(m_stream); try { m_traits.do_flush(); return S_OK; } COMET_CATCH_CLASS_INTERFACE_BOUNDARY("Commit", "adapted_stream"); } virtual HRESULT STDMETHODCALLTYPE Revert() { try { throw com_error("Transactions not supported", E_NOTIMPL); } COMET_CATCH_CLASS_INTERFACE_BOUNDARY("Revert", "adapted_stream"); } virtual HRESULT STDMETHODCALLTYPE LockRegion( ULARGE_INTEGER /*offset*/, ULARGE_INTEGER /*extent*/, DWORD /*lock_type*/) { try { throw com_error( "Locking not supported", STG_E_INVALIDFUNCTION); } COMET_CATCH_CLASS_INTERFACE_BOUNDARY("LockRegion", "adapted_stream"); } virtual HRESULT STDMETHODCALLTYPE UnlockRegion( ULARGE_INTEGER /*offset*/, ULARGE_INTEGER /*extent*/, DWORD /*lock_type*/) { try { throw com_error( "Locking not supported", STG_E_INVALIDFUNCTION); } COMET_CATCH_CLASS_INTERFACE_BOUNDARY("UnlockRegion", "adapted_stream"); } /** * Get some metadata about the stream. * * The name returned (if requested) is the name optionally given * in the constructor. If not given in the constructor, an * empty string is returned. * * Some fields, such as the date fields, are not valid as that data * is not available for IOStreams. */ virtual HRESULT STDMETHODCALLTYPE Stat( STATSTG* attributes_out, DWORD stat_flag) { try { if (!attributes_out) { throw com_error("STATSTG not given", STG_E_INVALIDPOINTER); } *attributes_out = STATSTG(); attributes_out->type = STGTY_STREAM; try { attributes_out->cbSize.QuadPart = impl::stream_size(m_traits); } catch (const std::exception&) { // Swallow non-bad errors as many stream are not seekable and // therefore not sizeable if (m_stream.bad()) throw; } // Must be last as, after we detach, any failure will leak // memory if (!(stat_flag & STATFLAG_NONAME)) { // pwcsName is NOT a BSTR. It's a null-terminated OLESTR // managed by the COM memory allocator size_t buffer_size = m_optional_name.size() + 1; size_t buffer_size_in_bytes = buffer_size * sizeof(wchar_t); attributes_out->pwcsName = static_cast( ::CoTaskMemAlloc(buffer_size_in_bytes)); if (!attributes_out->pwcsName) { throw com_error( "Unable to allocate memory for stream name", STG_E_INSUFFICIENTMEMORY); } try { HRESULT hr = ::StringCbCopyW( attributes_out->pwcsName, buffer_size_in_bytes, m_optional_name.c_str()); if (FAILED(hr)) { throw com_error( "Unable to copy stream name to STATSTG", hr); } } catch(...) { ::CoTaskMemFree(attributes_out->pwcsName); throw; } } return S_OK; } COMET_CATCH_CLASS_INTERFACE_BOUNDARY("Stat", "adapted_stream"); } /** * Cloning not supported for IOStreams as they are not copyable. */ virtual HRESULT STDMETHODCALLTYPE Clone(IStream** stream_out) { if (stream_out) { *stream_out = NULL; } try { throw com_error( "Cloning not supported", STG_E_INVALIDFUNCTION); } COMET_CATCH_CLASS_INTERFACE_BOUNDARY("Clone", "adapted_stream"); } private: Stream& m_stream; stream_traits_type m_traits; bstr_t m_optional_name; }; template class adapted_stream_pointer : public simple_object { public: adapted_stream_pointer(StreamPtr stream, const bstr_t& optional_name) : m_stream(stream), m_inner(adapt_stream(*m_stream, optional_name)) {} // The forwarded methods must return their HRESULT by throwing and // catching in order to propagate the IErrorInfo upwards. virtual HRESULT STDMETHODCALLTYPE Read( void* buffer, ULONG buffer_size, ULONG* read_count_out) { if (read_count_out) { *read_count_out = 0U; } try { HRESULT hr = m_inner->Read(buffer, buffer_size, read_count_out); if (FAILED(hr)) throw com_error_from_interface(m_inner, hr); return hr; } COMET_CATCH_CLASS_INTERFACE_BOUNDARY( "Read", "adapted_stream_pointer"); } virtual HRESULT STDMETHODCALLTYPE Write( const void* buffer, ULONG buffer_size, ULONG* written_count_out) { if (written_count_out) { *written_count_out = 0U; } try { HRESULT hr = m_inner->Write( buffer, buffer_size, written_count_out); if (FAILED(hr)) throw com_error_from_interface(m_inner, hr); return hr; } COMET_CATCH_CLASS_INTERFACE_BOUNDARY( "Write", "adapted_stream_pointer"); } virtual HRESULT STDMETHODCALLTYPE Seek( LARGE_INTEGER offset, DWORD origin, ULARGE_INTEGER* new_position_out) { if (new_position_out) { new_position_out->QuadPart = 0U; } try { HRESULT hr = m_inner->Seek(offset, origin, new_position_out); if (FAILED(hr)) throw com_error_from_interface(m_inner, hr); return hr; } COMET_CATCH_CLASS_INTERFACE_BOUNDARY( "Seek", "adapted_stream_pointer"); } virtual HRESULT STDMETHODCALLTYPE SetSize(ULARGE_INTEGER new_size) { try { HRESULT hr = m_inner->SetSize(new_size); if (FAILED(hr)) throw com_error_from_interface(m_inner, hr); return hr; } COMET_CATCH_CLASS_INTERFACE_BOUNDARY( "SetSize", "adapted_stream_pointer"); } virtual HRESULT STDMETHODCALLTYPE CopyTo( IStream* destination, ULARGE_INTEGER amount, ULARGE_INTEGER* bytes_read_out, ULARGE_INTEGER* bytes_written_out) { if (bytes_read_out) { bytes_read_out->QuadPart = 0U; } if (bytes_written_out) { bytes_written_out->QuadPart = 0U; } try { HRESULT hr = m_inner->CopyTo( destination, amount, bytes_read_out, bytes_written_out); if (FAILED(hr)) throw com_error_from_interface(m_inner, hr); return hr; } COMET_CATCH_CLASS_INTERFACE_BOUNDARY( "CopyTo", "adapted_stream_pointer"); } virtual HRESULT STDMETHODCALLTYPE Commit(DWORD commit_flags) { try { HRESULT hr = m_inner->Commit(commit_flags); if (FAILED(hr)) throw com_error_from_interface(m_inner, hr); return hr; } COMET_CATCH_CLASS_INTERFACE_BOUNDARY( "Commit", "adapted_stream_pointer"); } virtual HRESULT STDMETHODCALLTYPE Revert() { try { HRESULT hr = m_inner->Revert(); if (FAILED(hr)) throw com_error_from_interface(m_inner, hr); return hr; } COMET_CATCH_CLASS_INTERFACE_BOUNDARY( "Revert", "adapted_stream_pointer"); } virtual HRESULT STDMETHODCALLTYPE LockRegion( ULARGE_INTEGER offset, ULARGE_INTEGER extent, DWORD lock_type) { try { HRESULT hr = m_inner->LockRegion(offset, extent, lock_type); if (FAILED(hr)) throw com_error_from_interface(m_inner, hr); return hr; } COMET_CATCH_CLASS_INTERFACE_BOUNDARY( "LockRegion", "adapted_stream_pointer"); } virtual HRESULT STDMETHODCALLTYPE UnlockRegion( ULARGE_INTEGER offset, ULARGE_INTEGER extent, DWORD lock_type) { try { HRESULT hr = m_inner->UnlockRegion(offset, extent, lock_type); if (FAILED(hr)) throw com_error_from_interface(m_inner, hr); return hr; } COMET_CATCH_CLASS_INTERFACE_BOUNDARY( "UnlockRegion", "adapted_stream_pointer"); } virtual HRESULT STDMETHODCALLTYPE Stat( STATSTG* attributes_out, DWORD stat_flag) { if (attributes_out) { *attributes_out = STATSTG(); } try { HRESULT hr = m_inner->Stat(attributes_out, stat_flag); if (FAILED(hr)) throw com_error_from_interface(m_inner, hr); return hr; } COMET_CATCH_CLASS_INTERFACE_BOUNDARY( "Stat", "adapted_stream_pointer"); } virtual HRESULT STDMETHODCALLTYPE Clone(IStream** stream_out) { if (stream_out) { *stream_out = NULL; } try { HRESULT hr = m_inner->Clone(stream_out); if (FAILED(hr)) throw com_error_from_interface(m_inner, hr); return hr; } COMET_CATCH_CLASS_INTERFACE_BOUNDARY( "Clone", "adapted_stream_pointer"); } private: StreamPtr m_stream; com_ptr m_inner; }; } /** * Wrap COM IStream interface around C++ IOStream. * * The caller must ensure that the C++ IOStream remains valid until the * last reference to the returned wrapper is released. * * Unlike C++ streams which may have separate read and write positions that * move independently, COM IStreams assume a single combined read/write head. * Therefore this wrapper always starts the next read or write operation * from the where the last operation finished, regardless of whether that * operation was a call to `Read` or `Write`. * * @note This only applies for as long as the read/write positions are * modified only via this wrapper. If the positions are modified by * directly on the underlying IOStream, it is undefined whether the * starting point for the next call to `Read`/`Write` is syncronised * with the end of the previous operation. * * If operations on the inner stream results in failure (the `failbit` * is set), this is communicated via the COM-interface return code. The * `failbit` is cleared before the call returns. This allows further * wrapper methods to be called without having the clear the bit directly * on the underlying stream. Fatal errors (`badbit`) and end-of-file * (`eofbit`) are left unchanged and remain visible in the underlying * stream. */ template inline com_ptr adapt_stream( Stream& stream, const bstr_t& optional_name=bstr_t()) { return new impl::adapted_stream(stream, optional_name); } /** * Wrap COM IStream interface around pointer (usually smart) to C++ IOStream. * * If the pointer type is a smart pointer, the caller need not must ensure * the lifetime of the C++ IOStream exceeds that of the adapter; the smart * pointer takes care of ensuring this. * * The main reason for having this function in addition to `adapt_stream` is * support the common case where a COM stream is intended to be the sole owner * of the C++ stream with which it is created. This allows the caller to * create the IStream and forget about the C++ stream. Using `adapt_stream` * they would have to manage the lifetime of both. * * When we support movable types (C++11 or Boost.Move emulation), this method * would become unnecessary for this purpose as the C++ stream could simply * be moved into the adapter */ template inline com_ptr adapt_stream_pointer( StreamPtr stream_pointer, const bstr_t& optional_name=bstr_t()) { return new impl::adapted_stream_pointer( stream_pointer, optional_name); } } #endif