// Copyright 2022 The Pigweed Authors // // Licensed under the Apache License, Version 2.0 (the "License"); you may not // use this file except in compliance with the License. You may obtain a copy of // the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations under // the License. #include "pw_stream/std_file_stream.h" #include #include #include #include #include #include #include #include #include "pw_assert/assert.h" #include "pw_bytes/span.h" #include "pw_containers/algorithm.h" #include "pw_random/xor_shift.h" #include "pw_span/span.h" #include "pw_status/status.h" #include "pw_status/status_with_size.h" #include "pw_string/string_builder.h" #include "pw_unit_test/framework.h" namespace pw::stream { namespace { constexpr std::string_view kSmallTestData( "This is a test string used to verify correctness!"); // Creates a directory with a specified prefix followed by a random 32-bit hex // number. Random temporary file handle names can then be requested. When the // TempDir is destroyed, the entire directory is deleted. // // Example created temporary files: // /tmp/StdFileStreamTest32B37409/997BDDA2 // /tmp/StdFileStreamTest32B37409/C181909B // // WARNING: This class should ONLY be used for these tests! // // These tests need to open and close files by file name, which is incompatible // with std::tmpfile() (which deletes files on close). Even though std::tmpnam() // looks like the right tool to use, it's not thread safe and doesn't provide // any guarantees that the provided file name is not in use. std::tmpnam() is // also marked with a deprecation warning on some systems, warning against using // it at all. // // While on some systems this approach may provide significantly better // uniqueness since std::random_device may be backed with thread-safe random // sources, the STL does not explicitly require std::random_device to produce // non-deterministic random data (instead only recommending it). If // std::random_device is pseudo-random, this temporary directory will always // end up with the same naming pattern. // // If the STL required std::random_device to be thread-safe and // cryptographically-secure, this class could be made reasonably production // ready by increasing use of entropy and making temporary file name selection // thread-safe (in case a TempDir is static and shared across multiple threads). // // Today, this class does not provide much better safety guarantees than // std::tmpnam(), but thanks to the required directory prefix and typical // implementations of std::random_device, should see less risk of collisions in // practice. class TempDir { public: TempDir(std::string_view prefix) : rng_(GetSeed()) { temp_dir_ = std::filesystem::temp_directory_path(); temp_dir_ /= std::string(prefix) + GetRandomSuffix(); PW_ASSERT(std::filesystem::create_directory(temp_dir_)); } ~TempDir() { PW_ASSERT(std::filesystem::remove_all(temp_dir_)); } std::filesystem::path GetTempFileName() { return temp_dir_ / GetRandomSuffix(); } private: std::string GetRandomSuffix() { pw::StringBuffer<9> random_suffix_str; uint32_t random_suffix_int = 0; rng_.GetInt(random_suffix_int); PW_ASSERT(random_suffix_str.Format("%08" PRIx32, random_suffix_int).ok()); return std::string(random_suffix_str.view()); } // Generate a 64-bit random from system entropy pool. This is used to seed a // pseudo-random number generator for individual file names. static uint64_t GetSeed() { std::random_device sys_rand; uint64_t seed = 0; for (size_t seed_bytes = 0; seed_bytes < sizeof(seed); seed_bytes += sizeof(std::random_device::result_type)) { std::random_device::result_type val = sys_rand(); seed = seed << 8 * sizeof(std::random_device::result_type); seed |= val; } return seed; } random::XorShiftStarRng64 rng_; std::filesystem::path temp_dir_; }; class StdFileStreamTest : public ::testing::Test { protected: StdFileStreamTest() = default; void SetUp() override { temp_file_path_ = temp_dir_.GetTempFileName().generic_string(); } void TearDown() override { PW_ASSERT(std::filesystem::remove(TempFilename())); } const char* TempFilename() { return temp_file_path_.c_str(); } private: // Only construct one temporary directory to reduce waste of system entropy. static TempDir temp_dir_; std::string temp_file_path_; }; TempDir StdFileStreamTest::temp_dir_{"StdFileStreamTest"}; TEST_F(StdFileStreamTest, SeekAtEnd) { // Write some data to the temporary file. const std::string_view kTestData = kSmallTestData; StdFileWriter writer(TempFilename()); ASSERT_EQ(writer.Write(as_bytes(span(kTestData))), OkStatus()); writer.Close(); StdFileReader reader(TempFilename()); ASSERT_EQ(reader.ConservativeReadLimit(), kTestData.size()); std::array read_buffer; size_t read_offset = 0; while (read_offset < kTestData.size()) { Result result = reader.Read(as_writable_bytes(span(read_buffer))); ASSERT_EQ(result.status(), OkStatus()); ASSERT_GT(result.value().size(), 0u); ASSERT_LE(result.value().size(), read_buffer.size()); ASSERT_LE(result.value().size(), kTestData.size() - read_offset); ConstByteSpan expect_window = as_bytes(span(kTestData)).subspan(read_offset, result.value().size()); EXPECT_TRUE(pw::containers::Equal(result.value(), expect_window)); read_offset += result.value().size(); ASSERT_EQ(reader.ConservativeReadLimit(), kTestData.size() - read_offset); } // After data has been read, do a final read to trigger EOF. Result result = reader.Read(as_writable_bytes(span(read_buffer))); EXPECT_EQ(result.status(), Status::OutOfRange()); ASSERT_EQ(reader.ConservativeReadLimit(), 0u); EXPECT_EQ(read_offset, kTestData.size()); // Seek backwards and read again to ensure seek at EOF works. ASSERT_EQ(reader.Seek(-1 * read_buffer.size(), Stream::Whence::kEnd), OkStatus()); ASSERT_EQ(reader.ConservativeReadLimit(), read_buffer.size()); result = reader.Read(as_writable_bytes(span(read_buffer))); EXPECT_EQ(result.status(), OkStatus()); reader.Close(); } } // namespace } // namespace pw::stream