1//! @file io.cpp
2//! @author ryftchen
3//! @brief The definitions (io) in the utility module.
4//! @version 0.1.0
5//! @copyright Copyright (c) 2022-2026 ryftchen. All rights reserved.
6
7#include "io.hpp"
8
9#include <sys/epoll.h>
10#include <sys/file.h>
11#include <unistd.h>
12#include <cstring>
13#include <deque>
14#include <filesystem>
15#include <iostream>
16#include <ranges>
17
18namespace utility::io
19{
20//! @brief Function version number.
21//! @return version number (major.minor.patch)
22const char* version() noexcept
23{
24 static const char* const ver = "0.1.0";
25 return ver;
26}
27
28//! @brief Execute the command line.
29//! @param command - target command line to be executed
30//! @return command line output
31std::string executeCommand(const std::string& command)
32{
33 auto* const pipe = ::popen(command: command.c_str(), modes: "r"); // NOLINT(cert-env33-c)
34 if (!pipe)
35 {
36 throw std::runtime_error{"Could not open pipe when trying to execute command."};
37 }
38
39 std::string output{};
40 for (std::vector<char> buffer(4096);;)
41 {
42 const std::size_t readLen = std::fread(ptr: buffer.data(), size: sizeof(char), n: buffer.size(), stream: pipe);
43 if (readLen == 0)
44 {
45 break;
46 }
47 output.append(s: buffer.data(), n: readLen);
48 }
49
50 const int status = ::pclose(stream: pipe);
51 if (WIFEXITED(status) && (WEXITSTATUS(status) != EXIT_SUCCESS))
52 {
53 throw std::runtime_error{"The command returned exit code " + std::to_string(WEXITSTATUS(status)) + '.'};
54 }
55 if (WIFSIGNALED(status))
56 {
57 throw std::runtime_error{"The command was terminated by signal " + std::to_string(WTERMSIG(status)) + '.'};
58 }
59 return output;
60}
61
62//! @brief Wait for input from the user.
63//! @param operation - handling for inputs (interrupt waiting if the return value is true, otherwise continue waiting)
64//! @param timeout - timeout period (ms)
65void waitForUserInput(const std::function<bool(const std::string&)>& operation, const int timeout)
66{
67 const int epollFD = ::epoll_create1(flags: 0);
68 if (epollFD == -1)
69 {
70 throw std::runtime_error{"Could not create epoll when trying to wait for user input."};
71 }
72
73 ::epoll_event event{};
74 event.events = ::EPOLLIN;
75 event.data.fd = STDIN_FILENO;
76 if (::epoll_ctl(epfd: epollFD, EPOLL_CTL_ADD, STDIN_FILENO, event: &event) == -1)
77 {
78 ::close(fd: epollFD);
79 throw std::runtime_error{"Could not control epoll when trying to wait for user input."};
80 }
81
82 for (;;)
83 {
84 const int status = ::epoll_wait(epfd: epollFD, events: &event, maxevents: 1, timeout: timeout);
85 if (status == -1)
86 {
87 ::close(fd: epollFD);
88 throw std::runtime_error{"Failed to wait epoll when waiting for user input."};
89 }
90 if (status == 0)
91 {
92 break;
93 }
94
95 if ((event.events & ::EPOLLIN) != 0)
96 {
97 std::string input{};
98 std::getline(is&: std::cin, str&: input);
99 if (!operation || operation(input))
100 {
101 break;
102 }
103 }
104 }
105
106 ::close(fd: epollFD);
107}
108
109//! @brief Read lines from the file.
110//! @param filename - file path
111//! @param lock - lock or not
112//! @param reverse - reverse or not
113//! @param limit - maximum number of lines
114//! @return file contents
115std::vector<std::string> readFileLines(
116 const std::string_view filename, const bool lock, const bool reverse, const int limit)
117{
118 FileReader reader(filename);
119 reader.open();
120 if (lock)
121 {
122 reader.lock();
123 }
124
125 auto& stream = reader.stream();
126 stream.seekg(0, std::ios::beg);
127 std::vector<std::string> contents{};
128 if (std::string line{}; !reverse)
129 {
130 contents.reserve(n: (limit >= 0) ? limit : 1024);
131 while (std::getline(is&: stream, str&: line))
132 {
133 if ((limit >= 0) && (static_cast<int>(contents.size()) == limit))
134 {
135 break;
136 }
137 contents.emplace_back(args: std::move(line));
138 }
139 contents.shrink_to_fit();
140 }
141 else
142 {
143 std::deque<std::string> buffer{};
144 while (std::getline(is&: stream, str&: line))
145 {
146 buffer.emplace_back(args: std::move(line));
147 if ((limit >= 0) && (static_cast<int>(buffer.size()) > limit))
148 {
149 buffer.pop_front();
150 }
151 }
152 contents.reserve(n: buffer.size());
153 for (auto& temp : std::ranges::reverse_view(buffer))
154 {
155 contents.emplace_back(args: std::move(temp));
156 }
157 }
158 return contents;
159}
160
161FDStreamBuffer::~FDStreamBuffer()
162{
163 reset();
164}
165
166void FDStreamBuffer::set(const int newFD)
167{
168 if (fd == newFD)
169 {
170 return;
171 }
172
173 if (fd >= 0)
174 {
175 sync();
176 ::close(fd: fd);
177 }
178 setg(gbeg: nullptr, gnext: nullptr, gend: nullptr);
179 setp(pbeg: nullptr, pend: nullptr);
180 readBuffer.fill(u: 0);
181 writeBuffer.fill(u: 0);
182 fd = newFD;
183}
184
185void FDStreamBuffer::reset()
186{
187 if (fd < 0)
188 {
189 return;
190 }
191
192 flush();
193 ::close(fd: fd);
194 setg(gbeg: nullptr, gnext: nullptr, gend: nullptr);
195 setp(pbeg: nullptr, pend: nullptr);
196 readBuffer.fill(u: 0);
197 writeBuffer.fill(u: 0);
198 fd = -1;
199}
200
201int FDStreamBuffer::flush()
202{
203 if (fd < 0)
204 {
205 return 0;
206 }
207
208 const char* ptr = pbase();
209 while (ptr < pptr())
210 {
211 const ::ssize_t writtenSize = ::write(fd: fd, buf: ptr, n: pptr() - ptr);
212 if (writtenSize <= 0)
213 {
214 return -1;
215 }
216 ptr += writtenSize;
217 }
218 setp(pbeg: writeBuffer.data(), pend: writeBuffer.data() + writeBuffer.size());
219 return 0;
220}
221
222FDStreamBuffer::int_type FDStreamBuffer::underflow()
223{
224 if (gptr() != egptr())
225 {
226 throw std::runtime_error{"Read pointer has not reached the end of the buffer."};
227 }
228 if (fd < 0)
229 {
230 return traits_type::eof();
231 }
232
233 const ::ssize_t readSize = ::read(fd: fd, buf: readBuffer.data(), nbytes: readBuffer.size());
234 if (readSize <= 0)
235 {
236 return traits_type::eof();
237 }
238 setg(gbeg: readBuffer.data(), gnext: readBuffer.data(), gend: readBuffer.data() + readSize);
239 return traits_type::to_int_type(c: *gptr());
240}
241
242FDStreamBuffer::int_type FDStreamBuffer::overflow(const int_type c)
243{
244 if (pptr() != epptr())
245 {
246 throw std::runtime_error{"Write pointer has not reached the end of the buffer."};
247 }
248 if (fd < 0)
249 {
250 return traits_type::eof();
251 }
252
253 if (sync() == -1)
254 {
255 return traits_type::eof();
256 }
257 if (c != traits_type::eof())
258 {
259 *pptr() = traits_type::to_char_type(c: c);
260 pbump(n: 1);
261 }
262 return c;
263}
264
265int FDStreamBuffer::sync()
266{
267 return flush();
268}
269
270std::streampos FDStreamBuffer::seekoff(
271 const std::streamoff off, const std::ios_base::seekdir way, const std::ios_base::openmode mode)
272{
273 if ((fd < 0) || (((mode & std::ios_base::out) != 0) && (sync() == -1)))
274 {
275 return -1;
276 }
277
278 ::off_t newOffset = 0;
279 switch (way)
280 {
281 case std::ios_base::beg:
282 newOffset = off;
283 break;
284 case std::ios_base::cur:
285 newOffset = ((((mode & std::ios_base::in) != 0) && gptr()) ? (off - (egptr() - gptr())) : off)
286 + ::lseek(fd: fd, offset: 0, SEEK_CUR);
287 break;
288 case std::ios_base::end:
289 newOffset = off + ::lseek(fd: fd, offset: 0, SEEK_END);
290 break;
291 default:
292 return -1;
293 }
294
295 if (::lseek(fd: fd, offset: newOffset, SEEK_SET) == -1)
296 {
297 return -1;
298 }
299 setg(gbeg: nullptr, gnext: nullptr, gend: nullptr);
300 return newOffset;
301}
302
303std::streampos FDStreamBuffer::seekpos(const std::streampos sp, const std::ios_base::openmode mode)
304{
305 return seekoff(off: sp, way: std::ios_base::beg, mode);
306}
307
308std::streamsize FDStreamBuffer::showmanyc()
309{
310 return ((fd >= 0) && gptr() && egptr()) ? (egptr() - gptr()) : 0;
311}
312
313template <typename Stream>
314FileHandle<Stream>::~FileHandle()
315{
316 unlock();
317 close();
318}
319
320template <typename Stream>
321bool FileHandle<Stream>::isOpened() const
322{
323 return fd >= 0;
324}
325
326template <typename Stream>
327void FileHandle<Stream>::close()
328{
329 if (!isOpened())
330 {
331 return;
332 }
333
334 stmBuf.reset();
335 fd = -1;
336}
337
338template <typename Stream>
339bool FileHandle<Stream>::isLocked() const
340{
341 return lockActive;
342}
343
344template <typename Stream>
345void FileHandle<Stream>::unlock()
346{
347 if (!isLocked())
348 {
349 return;
350 }
351
352 ::flock(fd: fd, LOCK_UN);
353 lockActive = false;
354}
355
356template <typename Stream>
357Stream& FileHandle<Stream>::stream() noexcept
358{
359 return stm;
360}
361
362template <typename Stream>
363void FileHandle<Stream>::doOpen(const int flag, const std::string_view action)
364{
365 if (isOpened())
366 {
367 return;
368 }
369
370 fd = ::open(file: name.c_str(), oflag: flag, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
371 if (fd < 0)
372 {
373 throw std::runtime_error{
374 "Failed to open " + std::filesystem::path{name}.filename().string() + " file for " + action.data() + '.'};
375 }
376 stmBuf.set(fd);
377}
378
379template <typename Stream>
380void FileHandle<Stream>::doLock(const int mode, const std::string_view action)
381{
382 if (isLocked())
383 {
384 return;
385 }
386
387 if (::flock(fd: fd, operation: mode | LOCK_NB) != 0)
388 {
389 throw std::runtime_error{
390 "Failed to lock file descriptor " + std::to_string(val: fd) + " for " + action.data() + '.'};
391 }
392 lockActive = true;
393}
394
395template class FileHandle<std::istream>;
396template class FileHandle<std::ostream>;
397
398void FileReader::open()
399{
400 doOpen(O_CREAT | O_RDONLY, action: "reading");
401}
402
403void FileReader::lock()
404{
405 doLock(LOCK_SH, action: "reading");
406}
407
408void FileWriter::open(const bool overwrite)
409{
410 doOpen(O_CREAT | (overwrite ? O_TRUNC : O_APPEND) | O_WRONLY, action: "writing");
411}
412
413void FileWriter::lock()
414{
415 doLock(LOCK_EX, action: "writing");
416}
417} // namespace utility::io
418