Create TemporarySubDirectory class

Create class that automatically creates and deletes a temporary
subdirectory.

The subdirectory will be located beneath the system temporary directory
(such as /tmp).

Change-Id: Id14d33b47658f144ad17c6741ec36f58492a88e2
Signed-off-by: Shawn McCarney <shawnmm@us.ibm.com>
diff --git a/meson.build b/meson.build
index da7a277..7d5dd11 100644
--- a/meson.build
+++ b/meson.build
@@ -157,6 +157,7 @@
     'gpio.cpp',
     'pmbus.cpp',
     'temporary_file.cpp',
+    'temporary_subdirectory.cpp',
     'utility.cpp',
     dependencies: [
         nlohmann_json_dep,
diff --git a/temporary_subdirectory.cpp b/temporary_subdirectory.cpp
new file mode 100644
index 0000000..dbab102
--- /dev/null
+++ b/temporary_subdirectory.cpp
@@ -0,0 +1,81 @@
+/**
+ * Copyright © 2024 IBM Corporation
+ *
+ * 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
+ *
+ *     http://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 "temporary_subdirectory.hpp"
+
+#include <errno.h>  // for errno
+#include <stdlib.h> // for mkdtemp()
+#include <string.h> // for strerror()
+
+#include <stdexcept>
+#include <string>
+
+namespace phosphor::power::util
+{
+
+namespace fs = std::filesystem;
+
+TemporarySubDirectory::TemporarySubDirectory()
+{
+    // Build template path required by mkdtemp()
+    std::string templatePath = fs::temp_directory_path() /
+                               "phosphor-power-XXXXXX";
+
+    // Generate unique subdirectory name and create it.  The XXXXXX characters
+    // are replaced by mkdtemp() to make the subdirectory name unique.
+    char* retVal = mkdtemp(templatePath.data());
+    if (retVal == nullptr)
+    {
+        throw std::runtime_error{
+            std::string{"Unable to create temporary subdirectory: "} +
+            strerror(errno)};
+    }
+
+    // Store path to temporary subdirectory
+    path = templatePath;
+}
+
+TemporarySubDirectory&
+    TemporarySubDirectory::operator=(TemporarySubDirectory&& subdirectory)
+{
+    // Verify not assigning object to itself (a = std::move(a))
+    if (this != &subdirectory)
+    {
+        // Delete temporary subdirectory owned by this object
+        remove();
+
+        // Move subdirectory path from other object, transferring ownership
+        path = std::move(subdirectory.path);
+
+        // Clear path in other object; after move path is in unspecified state
+        subdirectory.path.clear();
+    }
+    return *this;
+}
+
+void TemporarySubDirectory::remove()
+{
+    if (!path.empty())
+    {
+        // Delete temporary subdirectory from file system
+        fs::remove_all(path);
+
+        // Clear path to indicate subdirectory has been deleted
+        path.clear();
+    }
+}
+
+} // namespace phosphor::power::util
diff --git a/temporary_subdirectory.hpp b/temporary_subdirectory.hpp
new file mode 100644
index 0000000..9e03153
--- /dev/null
+++ b/temporary_subdirectory.hpp
@@ -0,0 +1,131 @@
+/**
+ * Copyright © 2024 IBM Corporation
+ *
+ * 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
+ *
+ *     http://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.
+ */
+#pragma once
+
+#include <filesystem>
+#include <utility>
+
+namespace phosphor::power::util
+{
+
+/**
+ * @class TemporarySubDirectory
+ *
+ * A temporary subdirectory in the file system.
+ *
+ * This class does NOT represent the system temporary directory (such as /tmp).
+ * It represents a temporary subdirectory below that directory.
+ *
+ * The temporary subdirectory is created by the constructor.  The absolute path
+ * to the subdirectory can be obtained using getPath().
+ *
+ * The temporary subdirectory can be deleted by calling remove().  Otherwise the
+ * subdirectory will be deleted by the destructor.
+ *
+ * TemporarySubDirectory objects cannot be copied, but they can be moved.  This
+ * enables them to be stored in containers like std::vector.
+ */
+class TemporarySubDirectory
+{
+  public:
+    // Specify which compiler-generated methods we want
+    TemporarySubDirectory(const TemporarySubDirectory&) = delete;
+    TemporarySubDirectory& operator=(const TemporarySubDirectory&) = delete;
+
+    /**
+     * Constructor.
+     *
+     * Creates a temporary subdirectory below the system temporary directory
+     * (such as /tmp).
+     *
+     * Throws an exception if the subdirectory cannot be created.
+     */
+    TemporarySubDirectory();
+
+    /**
+     * Move constructor.
+     *
+     * Transfers ownership of a temporary subdirectory.
+     *
+     * @param subdirectory TemporarySubDirectory object being moved
+     */
+    TemporarySubDirectory(TemporarySubDirectory&& subdirectory) :
+        path{std::move(subdirectory.path)}
+    {
+        // Clear path in other object; after move path is in unspecified state
+        subdirectory.path.clear();
+    }
+
+    /**
+     * Move assignment operator.
+     *
+     * Deletes the temporary subdirectory owned by this object.  Then transfers
+     * ownership of the temporary subdirectory owned by the other object.
+     *
+     * Throws an exception if an error occurs during the deletion.
+     *
+     * @param subdirectory TemporarySubDirectory object being moved
+     */
+    TemporarySubDirectory& operator=(TemporarySubDirectory&& subdirectory);
+
+    /**
+     * Destructor.
+     *
+     * Deletes the temporary subdirectory if necessary.
+     */
+    ~TemporarySubDirectory()
+    {
+        try
+        {
+            remove();
+        }
+        catch (...)
+        {
+            // Destructors should not throw exceptions
+        }
+    }
+
+    /**
+     * Deletes the temporary subdirectory.
+     *
+     * Does nothing if the subdirectory has already been deleted.
+     *
+     * Throws an exception if an error occurs during the deletion.
+     */
+    void remove();
+
+    /**
+     * Returns the absolute path to the temporary subdirectory.
+     *
+     * Returns an empty path if the subdirectory has been deleted.
+     *
+     * @return temporary subdirectory path
+     */
+    const std::filesystem::path& getPath() const
+    {
+        return path;
+    }
+
+  private:
+    /**
+     * Absolute path to the temporary subdirectory.
+     *
+     * Empty when subdirectory has been deleted.
+     */
+    std::filesystem::path path{};
+};
+
+} // namespace phosphor::power::util
diff --git a/test/meson.build b/test/meson.build
index 0b4fd20..2e13604 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -5,6 +5,7 @@
         'file_descriptor_tests.cpp',
         'nvtest.cpp',
         'temporary_file_tests.cpp',
+        'temporary_subdirectory_tests.cpp',
         dependencies: [
             gmock,
             gtest,
diff --git a/test/temporary_subdirectory_tests.cpp b/test/temporary_subdirectory_tests.cpp
new file mode 100644
index 0000000..0b0188f
--- /dev/null
+++ b/test/temporary_subdirectory_tests.cpp
@@ -0,0 +1,284 @@
+/**
+ * Copyright © 2024 IBM Corporation
+ *
+ * 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
+ *
+ *     http://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 "temporary_subdirectory.hpp"
+
+#include <filesystem>
+#include <fstream>
+#include <string>
+#include <utility>
+
+#include <gtest/gtest.h>
+
+using namespace phosphor::power::util;
+namespace fs = std::filesystem;
+
+TEST(TemporarySubDirectoryTests, DefaultConstructor)
+{
+    TemporarySubDirectory subdirectory{};
+
+    fs::path path = subdirectory.getPath();
+    EXPECT_FALSE(path.empty());
+    EXPECT_TRUE(fs::exists(path));
+    EXPECT_TRUE(fs::is_directory(path));
+
+    fs::path parentDir = path.parent_path();
+    EXPECT_EQ(parentDir, "/tmp");
+
+    std::string baseName = path.filename();
+    EXPECT_TRUE(baseName.starts_with("phosphor-power-"));
+}
+
+TEST(TemporarySubDirectoryTests, MoveConstructor)
+{
+    // Create first object and verify subdirectory exists
+    TemporarySubDirectory subdirectory1{};
+    EXPECT_FALSE(subdirectory1.getPath().empty());
+    EXPECT_TRUE(fs::exists(subdirectory1.getPath()));
+
+    // Save path to subdirectory
+    fs::path path = subdirectory1.getPath();
+
+    // Create second object by moving first object
+    TemporarySubDirectory subdirectory2{std::move(subdirectory1)};
+
+    // Verify first object now has an empty path
+    EXPECT_TRUE(subdirectory1.getPath().empty());
+
+    // Verify second object now owns same subdirectory and subdirectory exists
+    EXPECT_EQ(subdirectory2.getPath(), path);
+    EXPECT_TRUE(fs::exists(subdirectory2.getPath()));
+}
+
+TEST(TemporarySubDirectoryTests, MoveAssignmentOperator)
+{
+    // Test where works: object is moved
+    {
+        // Create first object and verify subdirectory exists
+        TemporarySubDirectory subdirectory1{};
+        EXPECT_FALSE(subdirectory1.getPath().empty());
+        EXPECT_TRUE(fs::exists(subdirectory1.getPath()));
+
+        // Save path to first subdirectory
+        fs::path path1 = subdirectory1.getPath();
+
+        // Create second object and verify subdirectory exists
+        TemporarySubDirectory subdirectory2{};
+        EXPECT_FALSE(subdirectory2.getPath().empty());
+        EXPECT_TRUE(fs::exists(subdirectory2.getPath()));
+
+        // Save path to second subdirectory
+        fs::path path2 = subdirectory2.getPath();
+
+        // Verify temporary subdirectories are different
+        EXPECT_NE(path1, path2);
+
+        // Move first object into the second
+        subdirectory2 = std::move(subdirectory1);
+
+        // Verify first object now has an empty path
+        EXPECT_TRUE(subdirectory1.getPath().empty());
+
+        // Verify second object now owns first subdirectory and subdirectory
+        // exists
+        EXPECT_EQ(subdirectory2.getPath(), path1);
+        EXPECT_TRUE(fs::exists(path1));
+
+        // Verify second subdirectory was deleted
+        EXPECT_FALSE(fs::exists(path2));
+    }
+
+    // Test where does nothing: object moved into itself
+    {
+        // Create object and verify subdirectory exists
+        TemporarySubDirectory subdirectory{};
+        EXPECT_FALSE(subdirectory.getPath().empty());
+        EXPECT_TRUE(fs::exists(subdirectory.getPath()));
+
+        // Save path to subdirectory
+        fs::path path = subdirectory.getPath();
+
+        // Try to move object into itself; should do nothing
+        subdirectory = static_cast<TemporarySubDirectory&&>(subdirectory);
+
+        // Verify object still owns same subdirectory and subdirectory exists
+        EXPECT_EQ(subdirectory.getPath(), path);
+        EXPECT_TRUE(fs::exists(path));
+    }
+
+    // Test where fails: Cannot delete subdirectory
+    {
+        // Create first object and verify subdirectory exists
+        TemporarySubDirectory subdirectory1{};
+        EXPECT_FALSE(subdirectory1.getPath().empty());
+        EXPECT_TRUE(fs::exists(subdirectory1.getPath()));
+
+        // Save path to first subdirectory
+        fs::path path1 = subdirectory1.getPath();
+
+        // Create second object and verify subdirectory exists
+        TemporarySubDirectory subdirectory2{};
+        EXPECT_FALSE(subdirectory2.getPath().empty());
+        EXPECT_TRUE(fs::exists(subdirectory2.getPath()));
+
+        // Save path to second subdirectory
+        fs::path path2 = subdirectory2.getPath();
+
+        // Verify temporary subdirectories are different
+        EXPECT_NE(path1, path2);
+
+        // Change second subdirectory to unreadable so it cannot be removed
+        fs::permissions(path2, fs::perms::none);
+
+        try
+        {
+            // Try to move first object into the second; should throw exception
+            subdirectory2 = std::move(subdirectory1);
+            ADD_FAILURE() << "Should not have reached this line.";
+        }
+        catch (const std::exception& e)
+        {
+            // This is expected.  Exception message will vary.
+        }
+
+        // Change second subdirectory to readable/writable so it can be removed
+        fs::permissions(path2, fs::perms::owner_all);
+
+        // Verify first object has not changed and first subdirectory exists
+        EXPECT_EQ(subdirectory1.getPath(), path1);
+        EXPECT_TRUE(fs::exists(path1));
+
+        // Verify second object has not changed and second subdirectory exists
+        EXPECT_EQ(subdirectory2.getPath(), path2);
+        EXPECT_TRUE(fs::exists(path2));
+    }
+}
+
+TEST(TemporarySubDirectoryTests, Destructor)
+{
+    // Test where works: Subdirectory is deleted
+    {
+        fs::path path{};
+        {
+            TemporarySubDirectory subdirectory{};
+            path = subdirectory.getPath();
+            EXPECT_TRUE(fs::exists(path));
+        }
+        EXPECT_FALSE(fs::exists(path));
+    }
+
+    // Test where works: Subdirectory was already deleted
+    {
+        fs::path path{};
+        {
+            TemporarySubDirectory subdirectory{};
+            path = subdirectory.getPath();
+            EXPECT_TRUE(fs::exists(path));
+            subdirectory.remove();
+            EXPECT_FALSE(fs::exists(path));
+        }
+        EXPECT_FALSE(fs::exists(path));
+    }
+
+    // Test where fails: Cannot delete subdirectory: No exception thrown
+    {
+        fs::path path{};
+        try
+        {
+            TemporarySubDirectory subdirectory{};
+            path = subdirectory.getPath();
+            EXPECT_TRUE(fs::exists(path));
+
+            // Change subdirectory to unreadable so it cannot be removed
+            fs::permissions(path, fs::perms::none);
+        }
+        catch (...)
+        {
+            ADD_FAILURE() << "Should not have caught exception.";
+        }
+
+        // Change subdirectory to readable/writable so it can be removed
+        fs::permissions(path, fs::perms::owner_all);
+
+        // Subdirectory should still exist
+        EXPECT_TRUE(fs::exists(path));
+
+        // Delete subdirectory
+        fs::remove_all(path);
+    }
+}
+
+TEST(TemporarySubDirectoryTests, Remove)
+{
+    // Test where works
+    {
+        // Create object and verify subdirectory exists
+        TemporarySubDirectory subdirectory{};
+        EXPECT_FALSE(subdirectory.getPath().empty());
+        EXPECT_TRUE(fs::exists(subdirectory.getPath()));
+
+        // Save path to subdirectory
+        fs::path path = subdirectory.getPath();
+
+        // Delete subdirectory
+        subdirectory.remove();
+
+        // Verify path is cleared and subdirectory does not exist
+        EXPECT_TRUE(subdirectory.getPath().empty());
+        EXPECT_FALSE(fs::exists(path));
+
+        // Delete subdirectory again; should do nothing
+        subdirectory.remove();
+        EXPECT_TRUE(subdirectory.getPath().empty());
+        EXPECT_FALSE(fs::exists(path));
+    }
+
+    // Test where fails
+    {
+        // Create object and verify subdirectory exists
+        TemporarySubDirectory subdirectory{};
+        EXPECT_FALSE(subdirectory.getPath().empty());
+        EXPECT_TRUE(fs::exists(subdirectory.getPath()));
+
+        // Save path to subdirectory
+        fs::path path = subdirectory.getPath();
+
+        // Change subdirectory to unreadable so it cannot be removed
+        fs::permissions(path, fs::perms::none);
+
+        try
+        {
+            // Try to delete subdirectory; should fail with exception
+            subdirectory.remove();
+            ADD_FAILURE() << "Should not have reached this line.";
+        }
+        catch (const std::exception& e)
+        {
+            // This is expected.  Exception message will vary.
+        }
+
+        // Change subdirectory to readable/writable so it can be deleted by
+        // destructor
+        fs::permissions(path, fs::perms::owner_all);
+    }
+}
+
+TEST(TemporarySubDirectoryTests, GetPath)
+{
+    TemporarySubDirectory subdirectory{};
+    EXPECT_FALSE(subdirectory.getPath().empty());
+    EXPECT_EQ(subdirectory.getPath().parent_path(), "/tmp");
+    EXPECT_TRUE(fs::exists(subdirectory.getPath()));
+}