sustaining_gazes/lib/local/CamCom/comet/stream.h

1567 lines
53 KiB
C++

/** \file
* IStream adapters.
*/
/*
* Copyright (C) 2013 Alexander Lamaison <alexander.lamaison@gmail.com>
*
* 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 <comet/config.h>
#include <comet/bstr.h> // bstr_t
#include <comet/error.h> // com_error
#include <comet/handle_except.h> // COMET_CATCH_CLASS_INTERFACE_BOUNDARY
#include <comet/server.h> // simple_object
#include <cassert> // assert
#include <istream>
#include <limits> // numeric_limits
#include <ostream>
#include <strsafe.h> // 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<typename Stream>
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<char*>(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<ULONGLONG>(stream.gcount()) <
(std::numeric_limits<ULONG>::max)() /
sizeof(std::istream::char_type))
{
bytes_read_out = static_cast<ULONG>(
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<const char*>(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<typename Stream>
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<typename Stream>
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<typename StreamTraits>
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<typename StreamTraits>
inline std::streamoff stream_size(StreamTraits& traits)
{
position_resetter<StreamTraits>(traits, traits.do_tell());
std::streampos new_position;
traits.do_seek(0, std::ios_base::end, new_position);
return new_position;
}
template<typename Stream, bool is_istream, bool is_ostream>
class stream_traits;
template<typename Stream>
class stream_traits<Stream, true, false>
{
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<Stream> 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<typename Stream>
class stream_traits<Stream, false, true>
{
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<Stream> 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<typename Stream>
class stream_traits<Stream, true, true>
{
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<Stream> 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<Stream> 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<typename Stream>
class adapted_stream : public simple_object<IStream>
{
private:
typedef impl::stream_traits<
Stream,
type_traits::super_sub_class<std::istream, Stream>::result,
type_traits::super_sub_class<std::ostream, Stream>::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<Stream> 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<Stream> 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<Stream> 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<std::streamoff>::max)())
{
throw com_error(
"Seek offset too large", STG_E_INVALIDFUNCTION);
}
else if (offset.QuadPart <
(std::numeric_limits<std::streamoff>::min)())
{
throw com_error(
"Seek offset too small", STG_E_INVALIDFUNCTION);
}
else
{
try
{
m_traits.do_seek(
static_cast<std::streamoff>(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<Stream> state_resetter(m_stream);
position_resetter<stream_traits_type> 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<std::streamoff>::max)() < 0)
{
assert(!"Purely negative number!");
throw com_error(
"Seek offset too large", STG_E_INVALIDFUNCTION);
}
else if (new_end_position.QuadPart >
static_cast<ULONGLONG>(
(std::numeric_limits<std::streamoff>::max)()))
{
throw com_error(
"Seek offset too large", STG_E_INVALIDFUNCTION);
}
else
{
new_offset = static_cast<std::streamoff>(
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<Stream> 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<unsigned char> 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<ULONG>(
min(
(std::numeric_limits<ULONG>::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<Stream> 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<LPOLESTR>(
::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<typename StreamPtr>
class adapted_stream_pointer : public simple_object<IStream>
{
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<IStream> 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<typename Stream>
inline com_ptr<IStream> adapt_stream(
Stream& stream, const bstr_t& optional_name=bstr_t())
{
return new impl::adapted_stream<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<typename StreamPtr>
inline com_ptr<IStream> adapt_stream_pointer(
StreamPtr stream_pointer, const bstr_t& optional_name=bstr_t())
{
return new impl::adapted_stream_pointer<StreamPtr>(
stream_pointer, optional_name);
}
}
#endif