diff --git a/SConstruct b/SConstruct index 56792a8e..953d0bf9 100644 --- a/SConstruct +++ b/SConstruct @@ -342,6 +342,7 @@ libvmimetest_sources = [ [ 'tests/parser/pathTest', [ 'tests/parser/pathTest.cpp' ] ], [ 'tests/parser/parameterTest', [ 'tests/parser/parameterTest.cpp' ] ], [ 'tests/parser/textTest', [ 'tests/parser/textTest.cpp' ] ], + [ 'tests/utility/filteredStreamTest', [ 'tests/utility/filteredStreamTest.cpp' ] ], [ 'tests/utility/md5Test', [ 'tests/utility/md5Test.cpp' ] ], [ 'tests/utility/stringProxyTest', [ 'tests/utility/stringProxyTest.cpp' ] ], [ 'tests/utility/stringUtilsTest', [ 'tests/utility/stringUtilsTest.cpp' ] ], diff --git a/src/utility/filteredStream.cpp b/src/utility/filteredStream.cpp index d55e891e..c198c230 100644 --- a/src/utility/filteredStream.cpp +++ b/src/utility/filteredStream.cpp @@ -21,10 +21,99 @@ #include + namespace vmime { namespace utility { +// dotFilteredInputStream + +dotFilteredInputStream::dotFilteredInputStream(inputStream& is) + : m_stream(is) +{ +} + + +inputStream& dotFilteredInputStream::getPreviousInputStream() +{ + return (m_stream); +} + + +const bool dotFilteredInputStream::eof() const +{ + return (m_stream.eof()); +} + + +void dotFilteredInputStream::reset() +{ + m_previousChar2 = '\0'; + m_previousChar1 = '\0'; + + m_stream.reset(); +} + + +const stream::size_type dotFilteredInputStream::read(value_type* const data, const size_type count) +{ + const stream::size_type read = m_stream.read(data, count); + + const value_type* readPtr = data; + value_type* writePtr = data; + + const value_type* end = data + read; + + stream::size_type written = 0; + + // Replace "\n.." with "\n." + while (readPtr < end) + { + if (*readPtr == '.') + { + const value_type prevChar2 = + (readPtr == data + 1 ? m_previousChar1 : + readPtr == data ? m_previousChar2 : *(readPtr - 2)); + const value_type prevChar1 = + (readPtr == data ? m_previousChar1 : *(readPtr - 1)); + + if (prevChar2 == '\n' && prevChar1 == '.') + { + // Ignore last dot + } + else + { + *writePtr = *readPtr; + + ++writePtr; + ++written; + } + } + else + { + *writePtr = *readPtr; + + ++writePtr; + ++written; + } + + ++readPtr; + } + + m_previousChar2 = (read >= 2 ? data[read - 2] : m_previousChar1); + m_previousChar1 = (read >= 1 ? data[read - 1] : '\0'); + + return (written); +} + + +const stream::size_type dotFilteredInputStream::skip(const size_type /* count */) +{ + // Skipping bytes is not supported + return 0; +} + + // dotFilteredOutputStream dotFilteredOutputStream::dotFilteredOutputStream(outputStream& os) @@ -42,6 +131,9 @@ outputStream& dotFilteredOutputStream::getNextOutputStream() void dotFilteredOutputStream::write (const value_type* const data, const size_type count) { + if (count == 0) + return; + const value_type* pos = data; const value_type* end = data + count; const value_type* start = data; @@ -85,10 +177,25 @@ outputStream& CRLFToLFFilteredOutputStream::getNextOutputStream() void CRLFToLFFilteredOutputStream::write (const value_type* const data, const size_type count) { + if (count == 0) + return; + const value_type* pos = data; const value_type* end = data + count; const value_type* start = data; + // Warning: if the whole buffer finishes with '\r', this + // last character will not be written back... + // TODO: add a finalize() method? + if (m_previousChar == '\r') + { + if (*pos != '\n') + { + m_stream.write("\r", 1); // write back \r + m_previousChar = *pos; + } + } + // Replace "\r\n" (CRLF) with "\n" (LF) while ((pos = std::find(pos, end, '\n')) != end) { @@ -97,7 +204,9 @@ void CRLFToLFFilteredOutputStream::write if (previousChar == '\r') { - m_stream.write(start, pos - 1 - data); // do not write \r + if (pos != data) + m_stream.write(start, pos - 1 - data); // do not write \r + m_stream.write("\n", 1); start = pos + 1; @@ -106,8 +215,16 @@ void CRLFToLFFilteredOutputStream::write ++pos; } - m_stream.write(start, end - start); - m_previousChar = data[count - 1]; + if (data[count - 1] == '\r') + { + m_stream.write(start, end - start - 1); + m_previousChar = '\r'; + } + else + { + m_stream.write(start, end - start); + m_previousChar = data[count - 1]; + } } diff --git a/tests/utility/filteredStreamTest.cpp b/tests/utility/filteredStreamTest.cpp new file mode 100644 index 00000000..e2faf404 --- /dev/null +++ b/tests/utility/filteredStreamTest.cpp @@ -0,0 +1,166 @@ +// +// VMime library (http://www.vmime.org) +// Copyright (C) 2002-2005 Vincent Richard +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of +// the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +// + +#include "../lib/unit++/unit++.h" + +#include +#include +#include + +#include "vmime/vmime.hpp" +#include "vmime/platforms/posix/posixHandler.hpp" + +#include "vmime/utility/filteredStream.hpp" + +using namespace unitpp; + + +namespace +{ + class filteredStreamTest : public suite + { + class chunkInputStream : public vmime::utility::inputStream + { + private: + + std::vector m_chunks; + std::vector ::size_type m_index; + + public: + + chunkInputStream() : m_index(0) { } + + void addChunk(const std::string& chunk) { m_chunks.push_back(chunk); } + + const bool eof() const { return (m_index >= m_chunks.size()); } + void reset() { m_index = 0; } + + const size_type read(value_type* const data, const size_type /* count */) + { + const std::string chunk = m_chunks[m_index]; + + // Warning: 'count' should be larger than chunk length. + // This is OK for our tests. + std::copy(chunk.begin(), chunk.end(), data); + + ++m_index; + + return chunk.length(); + } + + const size_type skip(const size_type /* count */) + { + // Not supported + return 0; + } + }; + + + void testDotFilteredInputStreamHelper + (const std::string& number, const std::string& expected, + const std::string& c1, const std::string& c2 = "", + const std::string& c3 = "", const std::string& c4 = "") + { + chunkInputStream cis; + cis.addChunk(c1); + if (!c2.empty()) cis.addChunk(c2); + if (!c3.empty()) cis.addChunk(c3); + if (!c4.empty()) cis.addChunk(c4); + + vmime::utility::dotFilteredInputStream is(cis); + + std::ostringstream oss; + vmime::utility::outputStreamAdapter os(oss); + + vmime::utility::bufferedStreamCopy(is, os); + + assert_eq(number, expected, oss.str()); + } + + void testDotFilteredInputStream() + { + testDotFilteredInputStreamHelper("1", "foo\n.bar", "foo\n..bar"); + testDotFilteredInputStreamHelper("2", "foo\n.bar", "foo\n", "..bar"); + testDotFilteredInputStreamHelper("3", "foo\n.bar", "foo\n.", ".bar"); + testDotFilteredInputStreamHelper("4", "foo\n.bar", "foo\n..", "bar"); + testDotFilteredInputStreamHelper("5", "foo\n.bar", "foo\n", ".", ".bar"); + testDotFilteredInputStreamHelper("6", "foo\n.bar", "foo\n", ".", ".", "bar"); + } + + template + void testFilteredOutputStreamHelper + (const std::string& number, const std::string& expected, + const std::string& c1, const std::string& c2 = "", + const std::string& c3 = "", const std::string& c4 = "") + { + std::ostringstream oss; + vmime::utility::outputStreamAdapter os(oss); + + FILTER fos(os); + + fos.write(c1.data(), c1.length()); + if (!c2.empty()) fos.write(c2.data(), c2.length()); + if (!c3.empty()) fos.write(c3.data(), c3.length()); + if (!c4.empty()) fos.write(c4.data(), c4.length()); + + assert_eq(number, expected, oss.str()); + } + + void testDotFilteredOutputStream() + { + typedef vmime::utility::dotFilteredOutputStream FILTER; + + testFilteredOutputStreamHelper("1", "foo\n..bar", "foo\n.bar"); + testFilteredOutputStreamHelper("2", "foo\n..bar", "foo\n", ".bar"); + testFilteredOutputStreamHelper("3", "foo\n..bar", "foo", "\n.bar"); + testFilteredOutputStreamHelper("4", "foo\n..bar", "foo", "\n", ".bar"); + testFilteredOutputStreamHelper("5", "foo\n..bar", "foo", "\n", ".", "bar"); + } + + void testCRLFToLFFilteredOutputStream() + { + typedef vmime::utility::CRLFToLFFilteredOutputStream FILTER; + + testFilteredOutputStreamHelper("1", "foo\nbar", "foo\r\nbar"); + testFilteredOutputStreamHelper("2", "foo\nbar", "foo\r\n", "bar"); + testFilteredOutputStreamHelper("3", "foo\nbar", "foo\r", "\nbar"); + testFilteredOutputStreamHelper("4", "foo\nbar", "foo", "\r\nbar"); + testFilteredOutputStreamHelper("5", "foo\nbar", "foo", "\r", "\nbar"); + testFilteredOutputStreamHelper("6", "foo\nbar", "foo", "\r", "\n", "bar"); + } + + public: + + filteredStreamTest() : suite("vmime::utility::filteredStream") + { + // VMime initialization + vmime::platformDependant::setHandler(); + + add("dotFilteredInputStream", testcase(this, "dotFilteredInputStream", &filteredStreamTest::testDotFilteredInputStream)); + add("dotFilteredOutputStream", testcase(this, "dotFilteredOutputStream", &filteredStreamTest::testDotFilteredOutputStream)); + add("CRLFToLFFilteredOutputStream", testcase(this, "CRLFToLFFilteredOutputStream", &filteredStreamTest::testCRLFToLFFilteredOutputStream)); + + suite::main().add("vmime::utility::filteredStream", this); + } + + }; + + filteredStreamTest* theTest = new filteredStreamTest(); +} + diff --git a/vmime/utility/filteredStream.hpp b/vmime/utility/filteredStream.hpp index fc014e4e..61a4a1c9 100644 --- a/vmime/utility/filteredStream.hpp +++ b/vmime/utility/filteredStream.hpp @@ -58,6 +58,39 @@ public: }; +/** A filtered input stream which replaces "\n.." + * sequences with "\n." sequences. + */ + +class dotFilteredInputStream : public filteredInputStream +{ +public: + + /** Construct a new filter for the specified input stream. + * + * @param is stream from which to read data to be filtered + */ + dotFilteredInputStream(inputStream& is); + + inputStream& getPreviousInputStream(); + + const bool eof() const; + + void reset(); + + const size_type read(value_type* const data, const size_type count); + + const size_type skip(const size_type count); + +private: + + inputStream& m_stream; + + value_type m_previousChar2; // (N - 1)th character of previous buffer + value_type m_previousChar1; // (N)th (last) character of previous buffer +}; + + /** A filtered output stream which replaces "\n." * sequences with "\n.." sequences. */