Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ The release run heads these entries with the version and opens a fresh

## Unreleased

- Stream copies report failed reads and writes, and file writes check their
final flush. Saving a text file over its source reads the text before truncation.

- Shared text helpers keep ASCII matching independent of locale and reject
malformed UTF-8, empty replacement patterns and overflowing repetitions.
In-memory byte writes reject offsets that overflow their bounds checks.
Expand Down
3 changes: 1 addition & 2 deletions src/odr/file.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -346,8 +346,7 @@ void TextFile::save(const std::string &path) const {
if (!is_savable()) {
throw UnsupportedOperation();
}
std::ofstream out = internal::util::file::create(path);
out << text();
internal::util::file::write(text(), path);
}

void TextFile::save(std::ostream &out) const {
Expand Down
18 changes: 16 additions & 2 deletions src/odr/internal/common/temporary_file.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include <odr/internal/common/temporary_file.hpp>

#include <odr/exceptions.hpp>

#include <odr/internal/common/random.hpp>
#include <odr/internal/util/stream_util.hpp>

Expand Down Expand Up @@ -87,14 +89,26 @@ TemporaryDiskFile TemporaryDiskFileFactory::copy(std::istream &in) const {
if (!file.is_open()) {
file.clear();
file.open(file_path.string(), std::ios_base::out | std::ios_base::binary);
if (!file.is_open()) {
throw FileWriteError(file_path.string());
}
break;
}

file.close();
}

util::stream::pipe(in, file);
file.close();
try {
util::stream::pipe(in, file);
file.close();
if (!file) {
throw FileWriteError(file_path.string());
}
} catch (...) {
file.close();
remove_quietly(file_path);
throw;
}

return TemporaryDiskFile(file_path);
}
Expand Down
6 changes: 2 additions & 4 deletions src/odr/internal/encoding/detect.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include <odr/internal/encoding/detect.hpp>

#include <odr/internal/encoding/text_encoding_table.hpp>
#include <odr/internal/util/stream_util.hpp>

#include <istream>
#include <string_view>
Expand Down Expand Up @@ -49,10 +50,7 @@ bool encoding_holds_nul(const TextEncoding encoding) {

std::string encoding::read_probe(std::istream &in,
const std::size_t max_bytes) {
std::string result(max_bytes, '\0');
in.read(result.data(), static_cast<std::streamsize>(max_bytes));
result.resize(static_cast<std::size_t>(in.gcount()));
return result;
return util::stream::read(in, max_bytes);
}

TextEncoding encoding::detect(const std::string_view probe) {
Expand Down
11 changes: 2 additions & 9 deletions src/odr/internal/magic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <odr/internal/abstract/file.hpp>
#include <odr/internal/common/file.hpp>
#include <odr/internal/open_strategy.hpp>
#include <odr/internal/util/stream_util.hpp>
#include <odr/internal/util/string_util.hpp>

#include <istream>
Expand All @@ -18,14 +19,6 @@ namespace odr::internal {

namespace {

/// At most @p size bytes, cut back to what was actually read.
std::string read_head(std::istream &in, const std::size_t size) {
std::string result(size, '\0');
in.read(result.data(), static_cast<std::streamsize>(size));
result.resize(static_cast<std::size_t>(in.gcount()));
return result;
}

bool match_magic(const std::string &head, const std::string &pattern) {
const auto bytes = util::string::split(pattern, " ");
if (bytes.size() > head.size()) {
Expand Down Expand Up @@ -201,7 +194,7 @@ FileType magic::file_type(std::istream &in) {
// an enhanced metafile names itself at offset 40, so the head has to reach 44
static constexpr std::size_t head_size = 64;

return file_type(read_head(in, head_size));
return file_type(util::stream::read(in, head_size));
}

FileType magic::file_type(const abstract::File &file) {
Expand Down
24 changes: 14 additions & 10 deletions src/odr/internal/util/file_util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,17 @@ std::ifstream file::open(const std::string &path) {
std::size_t file::size(const std::string &path) {
std::ifstream in = open(path);
in.seekg(0, std::ios::end);
return static_cast<std::size_t>(in.tellg());
const auto end = in.tellg();
if (end < 0) {
throw FileReadError();
}
return static_cast<std::size_t>(end);
}

std::string file::read(const std::string &path) {
std::ifstream in = open(path);

std::string result;

in.seekg(0, std::ios::end);
result.reserve(in.tellg());
in.seekg(0, std::ios::beg);

result.assign(std::istreambuf_iterator(in), std::istreambuf_iterator<char>());

return result;
return stream::read(in);
}

void file::pipe(const std::string &path, std::ostream &out) {
Expand All @@ -54,11 +50,19 @@ std::ofstream file::create(const std::string &path) {
void file::write(const std::string &data, const std::string &path) {
std::ofstream out = create(path);
out << data;
out.close();
if (!out) {
throw FileWriteError(path);
}
}

void file::write(std::istream &in, const std::string &path) {
std::ofstream out = create(path);
stream::pipe(in, out);
out.close();
if (!out) {
throw FileWriteError(path);
}
}

} // namespace odr::internal::util
19 changes: 17 additions & 2 deletions src/odr/internal/util/stream_util.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#include <odr/internal/util/stream_util.hpp>

#include <array>
#include <iterator>
#include <limits>
#include <sstream>
#include <streambuf>

Expand All @@ -12,12 +12,21 @@ using int_type = std::streambuf::int_type;
static constexpr int_type eof = std::streambuf::traits_type::eof();

std::string stream::read(std::istream &in) {
return {std::istreambuf_iterator(in), {}};
std::ostringstream out;
pipe(in, out);
return std::move(out).str();
}

std::string stream::read(std::istream &in, const std::size_t size) {
if (size >
static_cast<std::size_t>(std::numeric_limits<std::streamsize>::max())) {
throw std::length_error("stream read is too large");
}
std::string result(size, '\0');
in.read(result.data(), static_cast<std::streamsize>(size));
if (in.bad() || (in.fail() && !in.eof())) {
throw std::ios_base::failure("stream read failed");
}
result.resize(static_cast<std::size_t>(in.gcount()));
return result;
}
Expand All @@ -29,11 +38,17 @@ void stream::pipe(std::istream &in, std::ostream &out) {

while (true) {
in.read(buffer.data(), BUFFER_SIZE);
if (in.bad() || (in.fail() && !in.eof())) {
throw std::ios_base::failure("stream read failed");
}
const auto read = in.gcount();
if (read == 0) {
break;
}
out.write(buffer.data(), read);
if (!out) {
throw std::ios_base::failure("stream write failed");
}
}
}

Expand Down
24 changes: 24 additions & 0 deletions test/src/file_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <odr/odr.hpp>

#include <odr/internal/common/file.hpp>
#include <odr/internal/common/random.hpp>
#include <odr/internal/common/temporary_file.hpp>
#include <odr/internal/util/file_util.hpp>
#include <odr/internal/util/stream_util.hpp>
Expand Down Expand Up @@ -155,6 +156,29 @@ TEST(File, temporary_copies_preserve_binary_bytes) {
EXPECT_EQ(internal::MemoryFile(copy).content(), data);
}

TEST(File, text_can_be_saved_over_its_disk_source) {
std::istringstream source("original text\n");
const auto temporary =
internal::TemporaryDiskFileFactory::system_default().copy(source);
const std::string path = temporary.disk_path()->string();
const TextFile text =
open(path, DecodeOptions::as(FileType::text_file)).as_text_file();
text.save(path);
EXPECT_EQ(internal::util::file::read(path), "original text\n");
}

TEST(File, failed_temporary_copies_leave_no_file) {
const internal::AbsPath directory(std::filesystem::temp_directory_path());
const std::string name = "odr-failed-copy-" + internal::random_string(12);
const internal::TemporaryDiskFileFactory factory(directory,
[name] { return name; });
std::istringstream source("bytes");
source.setstate(std::ios::badbit);
EXPECT_THROW(std::ignore = factory.copy(source), std::ios_base::failure);
EXPECT_FALSE(
std::filesystem::exists(directory.join(internal::RelPath(name)).path()));
}

/// A file inside an archive is named by its entry, not by the archive.
TEST(File, an_archive_entry_is_named_by_its_entry) {
internal::zip::ZipArchive zip;
Expand Down
22 changes: 22 additions & 0 deletions test/src/internal/util/stream_util_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,28 @@ TEST(Stream, delimiter_reads_preserve_failed_input) {
EXPECT_EQ(stream::read_until(in, ';', true), "second;");
}

TEST(Stream, copying_reports_input_and_output_failures) {
std::istringstream in(std::string(4097, 'x'));
std::ostringstream out;
stream::pipe(in, out);
EXPECT_EQ(out.str(), std::string(4097, 'x'));

in.clear();
in.seekg(0);
out.setstate(std::ios::badbit);
EXPECT_THROW(stream::pipe(in, out), std::ios_base::failure);

in.setstate(std::ios::badbit);
EXPECT_THROW(stream::read(in), std::ios_base::failure);
EXPECT_THROW(stream::read(in, 1), std::ios_base::failure);

class FailingBuffer final : public std::streambuf {
int_type underflow() override { throw std::runtime_error("read failure"); }
} buffer;
std::istream broken(&buffer);
EXPECT_THROW(stream::read(broken), std::ios_base::failure);
}

TEST(ByteStream, length_prefixed_reads_handle_chunks_and_stream_exceptions) {
const std::string data(4097, 'x');
std::istringstream in(data);
Expand Down
Loading