Move file IO to standalone class

Adds object oriented way to work with zlib. Error handling for file IO
is based on C++ exceptions.
Replaces printf-like output with direct writing. This prevents buffer
overflow in zlib during write operations.

Change-Id: I626be309250c623cd60021ee6c17518855a171a6
Signed-off-by: Artem Senichev <a.senichev@yadro.com>
diff --git a/src/log_file.cpp b/src/log_file.cpp
new file mode 100644
index 0000000..0f7a20a
--- /dev/null
+++ b/src/log_file.cpp
@@ -0,0 +1,76 @@
+/**
+ * @brief Log file.
+ *
+ * This file is part of HostLogger project.
+ *
+ * Copyright (c) 2020 YADRO
+ *
+ * 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 "log_file.hpp"
+
+#include "zlib_exception.hpp"
+
+LogFile::LogFile(const char* fileName)
+{
+    fd_ = gzopen(fileName, "w");
+    if (fd_ == Z_NULL)
+        throw ZlibException(ZlibException::Create, Z_ERRNO, fd_, fileName);
+    fileName_ = fileName;
+}
+
+LogFile::~LogFile()
+{
+    if (fd_ != Z_NULL)
+        gzclose_w(fd_);
+}
+
+void LogFile::close()
+{
+    if (fd_ != Z_NULL)
+    {
+        const int rc = gzclose_w(fd_);
+        if (rc != Z_OK)
+            throw ZlibException(ZlibException::Close, rc, fd_, fileName_);
+        fd_ = Z_NULL;
+        fileName_.clear();
+    }
+}
+
+void LogFile::write(time_t timeStamp, const std::string& message) const
+{
+    int rc;
+
+    // Convert time stamp and write it
+    tm tmLocal;
+    localtime_r(&timeStamp, &tmLocal);
+    rc = gzprintf(fd_, "[ %02i:%02i:%02i ]: ", tmLocal.tm_hour, tmLocal.tm_min,
+                  tmLocal.tm_sec);
+    if (rc <= 0)
+        throw ZlibException(ZlibException::Write, rc, fd_, fileName_);
+
+    // Write message
+    const size_t len = message.length();
+    if (len)
+    {
+        rc = gzwrite(fd_, message.data(), static_cast<unsigned int>(len));
+        if (rc <= 0)
+            throw ZlibException(ZlibException::Write, rc, fd_, fileName_);
+    }
+
+    // Write EOL
+    rc = gzputc(fd_, '\n');
+    if (rc <= 0)
+        throw ZlibException(ZlibException::Write, rc, fd_, fileName_);
+}
diff --git a/src/log_file.hpp b/src/log_file.hpp
new file mode 100644
index 0000000..8494f8b
--- /dev/null
+++ b/src/log_file.hpp
@@ -0,0 +1,67 @@
+/**
+ * @brief Log file.
+ *
+ * This file is part of HostLogger project.
+ *
+ * Copyright (c) 2020 YADRO
+ *
+ * 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 <zlib.h>
+
+#include <ctime>
+#include <string>
+
+/** @class LogFile
+ *  @brief Log file writer.
+ */
+class LogFile
+{
+  public:
+    /** @brief Constructor - open new file for writing logs.
+     *
+     *  @param[in] fileName - path to the file
+     *
+     *  @throw ZlibException in case of errors
+     */
+    LogFile(const char* fileName);
+
+    ~LogFile();
+
+    LogFile(const LogFile&) = delete;
+    LogFile& operator=(const LogFile&) = delete;
+
+    /** @brief Close file.
+     *
+     *  @throw ZlibException in case of errors
+     */
+    void close();
+
+    /** @brief Write log message to file.
+     *
+     *  @param[in] timeStamp - time stamp of the log message
+     *  @param[in] message - log message text
+     *
+     *  @throw ZlibException in case of errors
+     */
+    void write(time_t timeStamp, const std::string& message) const;
+
+  private:
+    /** @brief File name. */
+    std::string fileName_;
+    /** @brief zLib file descriptor. */
+    gzFile fd_;
+};
diff --git a/src/log_manager.cpp b/src/log_manager.cpp
index 66a08c6..fde2f6c 100644
--- a/src/log_manager.cpp
+++ b/src/log_manager.cpp
@@ -159,7 +159,7 @@
     if (logFile.empty())
         return EIO;
 
-    rc = storage_.write(logFile.c_str());
+    rc = storage_.save(logFile.c_str());
     if (rc != 0)
         return rc;
 
diff --git a/src/log_storage.cpp b/src/log_storage.cpp
index 96b0b12..ff9b213 100644
--- a/src/log_storage.cpp
+++ b/src/log_storage.cpp
@@ -21,15 +21,14 @@
 #include "log_storage.hpp"
 
 #include "config.hpp"
+#include "log_file.hpp"
 
 #include <fcntl.h>
 #include <string.h>
 #include <sys/stat.h>
 #include <unistd.h>
 
-LogStorage::LogStorage() : last_complete_(true)
-{
-}
+#include <exception>
 
 void LogStorage::parse(const char* data, size_t len)
 {
@@ -92,7 +91,7 @@
     return messages_.empty();
 }
 
-int LogStorage::write(const char* fileName) const
+int LogStorage::save(const char* fileName) const
 {
     int rc = 0;
 
@@ -102,57 +101,35 @@
         return 0;
     }
 
-    const gzFile fd = gzopen(fileName, "w");
-    if (fd == Z_NULL)
+    try
     {
-        rc = errno;
-        fprintf(stderr, "Unable to open file %s: error [%i] %s\n", fileName, rc,
-                strerror(rc));
-        return rc;
+        LogFile log(fileName);
+
+        // Write full datetime stamp as the first record
+        const time_t& tmStart = messages_.begin()->timeStamp;
+        tm tmLocal;
+        localtime_r(&tmStart, &tmLocal);
+        char tmText[20]; // size of "%F %T" asciiz (YYYY-MM-DD HH:MM:SS)
+        strftime(tmText, sizeof(tmText), "%F %T", &tmLocal);
+        std::string titleMsg = ">>> Log collection started at ";
+        titleMsg += tmText;
+        log.write(tmStart, titleMsg);
+
+        // Write messages
+        for (auto it = messages_.begin(); it != messages_.end(); ++it)
+            log.write(it->timeStamp, it->text);
+
+        log.close();
     }
-
-    // Write full datetime stamp as the first record
-    const time_t& logStartTime = messages_.begin()->timeStamp;
-    tm localTime = {0};
-    localtime_r(&logStartTime, &localTime);
-    char msgText[64];
-    snprintf(msgText, sizeof(msgText),
-             ">>> Log collection started at %02i.%02i.%i %02i:%02i:%02i",
-             localTime.tm_mday, localTime.tm_mon + 1, localTime.tm_year + 1900,
-             localTime.tm_hour, localTime.tm_min, localTime.tm_sec);
-    const Message startMsg = {logStartTime, msgText};
-    rc |= write(fd, startMsg);
-
-    // Write messages
-    for (auto it = messages_.begin(); rc == 0 && it != messages_.end(); ++it)
-        rc |= write(fd, *it);
-
-    rc = gzclose_w(fd);
-    if (rc != Z_OK)
-        fprintf(stderr, "Unable to close file %s: error [%i]\n", fileName, rc);
+    catch (std::exception& e)
+    {
+        rc = EIO;
+        fprintf(stderr, "%s\n", e.what());
+    }
 
     return rc;
 }
 
-int LogStorage::write(gzFile fd, const Message& msg) const
-{
-    // Convert timestamp to local time
-    tm localTime = {0};
-    localtime_r(&msg.timeStamp, &localTime);
-
-    // Write message to the file
-    const int rc =
-        gzprintf(fd, "[ %02i:%02i:%02i ]: %s\n", localTime.tm_hour,
-                 localTime.tm_min, localTime.tm_sec, msg.text.c_str());
-    if (rc <= 0)
-    {
-        fprintf(stderr, "Unable to write file: error [%i]\n", -rc);
-        return EIO;
-    }
-
-    return 0;
-}
-
 void LogStorage::shrink()
 {
     if (loggerConfig.storageSizeLimit)
diff --git a/src/log_storage.hpp b/src/log_storage.hpp
index 550afd9..5361c88 100644
--- a/src/log_storage.hpp
+++ b/src/log_storage.hpp
@@ -33,9 +33,6 @@
 class LogStorage
 {
   public:
-    /** @brief Constructor. */
-    LogStorage();
-
     /** @brief Parse input log stream and append messages to the storage.
      *
      *  @param[in] data - pointer to the message buffer
@@ -58,7 +55,7 @@
      *
      *  @return error code, 0 if operation completed successfully
      */
-    int write(const char* fileName) const;
+    int save(const char* fileName) const;
 
   private:
     /** @struct Message
@@ -79,15 +76,6 @@
      */
     void append(const char* msg, size_t len);
 
-    /** @brief Write message to the file.
-     *
-     *  @param[in] fd - descriptor of the file to write
-     *  @param[in] msg - message to write
-     *
-     *  @return error code, 0 if operation completed successfully
-     */
-    int write(gzFile fd, const Message& msg) const;
-
     /** @brief Shrink storage by removing oldest messages. */
     void shrink();
 
@@ -95,5 +83,5 @@
     /** @brief List of messages. */
     std::list<Message> messages_;
     /** @brief Flag to indicate that the last message is incomplete. */
-    bool last_complete_;
+    bool last_complete_ = true;
 };
diff --git a/src/zlib_exception.cpp b/src/zlib_exception.cpp
new file mode 100644
index 0000000..e88c7b3
--- /dev/null
+++ b/src/zlib_exception.cpp
@@ -0,0 +1,77 @@
+/**
+ * @brief zLib exception.
+ *
+ * This file is part of HostLogger project.
+ *
+ * Copyright (c) 2020 YADRO
+ *
+ * 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 "zlib_exception.hpp"
+
+#include <cstring>
+
+ZlibException::ZlibException(Operation op, int code, gzFile fd,
+                             const std::string& fileName)
+{
+    std::string details;
+    if (code == Z_ERRNO)
+    {
+        // System error
+        const int errCode = errno ? errno : EIO;
+        details = strerror(errCode);
+    }
+    else if (fd != Z_NULL)
+    {
+        // Try to get description from zLib
+        int lastErrCode = 0;
+        const char* lastErrDesc = gzerror(fd, &lastErrCode);
+        if (lastErrCode)
+        {
+            details = '[';
+            details += std::to_string(lastErrCode);
+            details += "] ";
+            details += lastErrDesc;
+        }
+    }
+    if (details.empty())
+    {
+        details = "Internal zlib error (code ";
+        details += std::to_string(code);
+        details += ')';
+    }
+
+    what_ = "Unable to ";
+    switch (op)
+    {
+        case Create:
+            what_ += "create";
+            break;
+        case Close:
+            what_ += "close";
+            break;
+        case Write:
+            what_ += "write";
+            break;
+    }
+    what_ += " file ";
+    what_ += fileName;
+    what_ += ": ";
+    what_ += details;
+}
+
+const char* ZlibException::what() const noexcept
+{
+    return what_.c_str();
+}
diff --git a/src/zlib_exception.hpp b/src/zlib_exception.hpp
new file mode 100644
index 0000000..b147301
--- /dev/null
+++ b/src/zlib_exception.hpp
@@ -0,0 +1,58 @@
+/**
+ * @brief zLib exception.
+ *
+ * This file is part of HostLogger project.
+ *
+ * Copyright (c) 2020 YADRO
+ *
+ * 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 <zlib.h>
+
+#include <exception>
+#include <string>
+
+/** @class ZlibException
+ *  @brief zLib exception.
+ */
+class ZlibException : public std::exception
+{
+  public:
+    /** @brief File operation types. */
+    enum Operation
+    {
+        Create,
+        Write,
+        Close
+    };
+
+    /** @brief Constructor.
+     *
+     *  @param[in] op - type of operation
+     *  @param[in] code - zLib status code
+     *  @param[in] fd - zLib file descriptor
+     *  @param[in] fileName - file name
+     */
+    ZlibException(Operation op, int code, gzFile fd,
+                  const std::string& fileName);
+
+    // From std::exception
+    const char* what() const noexcept override;
+
+  private:
+    /** @brief Error description buffer. */
+    std::string what_;
+};