Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

file_util.cxx 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. /*
  2. * Copyright 2023 Vsevolod Stakhov
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #include "file_util.hxx"
  17. #include <fmt/core.h>
  18. #include "libutil/util.h"
  19. #include "libutil/unix-std.h"
  20. #define DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL
  21. #include "doctest/doctest.h"
  22. namespace rspamd::util {
  23. auto raii_file::open(const char *fname, int flags) -> tl::expected<raii_file, error>
  24. {
  25. int oflags = flags;
  26. #ifdef O_CLOEXEC
  27. oflags |= O_CLOEXEC;
  28. #endif
  29. if (fname == nullptr) {
  30. return tl::make_unexpected(error{"cannot open file; filename is nullptr", EINVAL, error_category::CRITICAL});
  31. }
  32. auto fd = ::open(fname, oflags);
  33. if (fd == -1) {
  34. return tl::make_unexpected(error{fmt::format("cannot open file {}: {}", fname, ::strerror(errno)), errno});
  35. }
  36. auto ret = raii_file{fname, fd, false};
  37. if (fstat(ret.fd, &ret.st) == -1) {
  38. return tl::make_unexpected(error{fmt::format("cannot stat file {}: {}", fname, ::strerror(errno)), errno});
  39. }
  40. return ret;
  41. }
  42. auto raii_file::create(const char *fname, int flags, int perms) -> tl::expected<raii_file, error>
  43. {
  44. int oflags = flags | O_CREAT;
  45. #ifdef O_CLOEXEC
  46. oflags |= O_CLOEXEC;
  47. #endif
  48. if (fname == nullptr) {
  49. return tl::make_unexpected(error{"cannot create file; filename is nullptr", EINVAL, error_category::CRITICAL});
  50. }
  51. auto fd = ::open(fname, oflags, perms);
  52. if (fd == -1) {
  53. return tl::make_unexpected(error{fmt::format("cannot create file {}: {}", fname, ::strerror(errno)), errno});
  54. }
  55. auto ret = raii_file{fname, fd, false};
  56. if (fstat(ret.fd, &ret.st) == -1) {
  57. return tl::make_unexpected(error{fmt::format("cannot stat file {}: {}", fname, ::strerror(errno)), errno});
  58. }
  59. return ret;
  60. }
  61. auto raii_file::create_temp(const char *fname, int flags, int perms) -> tl::expected<raii_file, error>
  62. {
  63. int oflags = flags;
  64. #ifdef O_CLOEXEC
  65. oflags |= O_CLOEXEC | O_CREAT | O_EXCL;
  66. #endif
  67. if (fname == nullptr) {
  68. return tl::make_unexpected(error{"cannot open file; filename is nullptr", EINVAL, error_category::CRITICAL});
  69. }
  70. auto fd = ::open(fname, oflags, perms);
  71. if (fd == -1) {
  72. return tl::make_unexpected(error{fmt::format("cannot create file {}: {}", fname, ::strerror(errno)), errno});
  73. }
  74. auto ret = raii_file{fname, fd, true};
  75. if (fstat(ret.fd, &ret.st) == -1) {
  76. return tl::make_unexpected(error{fmt::format("cannot stat file {}: {}", fname, ::strerror(errno)), errno});
  77. }
  78. return ret;
  79. }
  80. auto raii_file::mkstemp(const char *pattern, int flags, int perms) -> tl::expected<raii_file, error>
  81. {
  82. int oflags = flags;
  83. #ifdef O_CLOEXEC
  84. oflags |= O_CLOEXEC | O_CREAT | O_EXCL;
  85. #endif
  86. if (pattern == nullptr) {
  87. return tl::make_unexpected(error{"cannot open file; pattern is nullptr", EINVAL, error_category::CRITICAL});
  88. }
  89. std::string mutable_pattern = pattern;
  90. auto fd = g_mkstemp_full(mutable_pattern.data(), oflags, perms);
  91. if (fd == -1) {
  92. return tl::make_unexpected(error{fmt::format("cannot create file {}: {}", pattern, ::strerror(errno)), errno});
  93. }
  94. auto ret = raii_file{mutable_pattern.c_str(), fd, true};
  95. if (fstat(ret.fd, &ret.st) == -1) {
  96. return tl::make_unexpected(error{fmt::format("cannot stat file {}: {}",
  97. mutable_pattern, ::strerror(errno)),
  98. errno});
  99. }
  100. return ret;
  101. }
  102. raii_file::~raii_file() noexcept
  103. {
  104. if (fd != -1) {
  105. if (temp) {
  106. (void) unlink(fname.c_str());
  107. }
  108. close(fd);
  109. }
  110. }
  111. auto raii_file::update_stat() noexcept -> bool
  112. {
  113. return fstat(fd, &st) != -1;
  114. }
  115. raii_file::raii_file(const char *fname, int fd, bool temp)
  116. : fd(fd), temp(temp)
  117. {
  118. std::size_t nsz;
  119. /* Normalize path */
  120. this->fname = fname;
  121. rspamd_normalize_path_inplace(this->fname.data(), this->fname.size(), &nsz);
  122. this->fname.resize(nsz);
  123. }
  124. raii_locked_file::~raii_locked_file() noexcept
  125. {
  126. if (fd != -1) {
  127. (void) rspamd_file_unlock(fd, FALSE);
  128. }
  129. }
  130. auto raii_locked_file::lock_raii_file(raii_file &&unlocked) -> tl::expected<raii_locked_file, error>
  131. {
  132. if (!rspamd_file_lock(unlocked.get_fd(), TRUE)) {
  133. return tl::make_unexpected(
  134. error{fmt::format("cannot lock file {}: {}", unlocked.get_name(), ::strerror(errno)), errno});
  135. }
  136. return raii_locked_file{std::move(unlocked)};
  137. }
  138. auto raii_locked_file::unlock() -> raii_file
  139. {
  140. if (fd != -1) {
  141. (void) rspamd_file_unlock(fd, FALSE);
  142. }
  143. return raii_file{static_cast<raii_file &&>(std::move(*this))};
  144. }
  145. raii_mmaped_file::raii_mmaped_file(raii_file &&file, void *map, std::size_t sz)
  146. : file(std::move(file)), map(map), map_size(sz)
  147. {
  148. }
  149. auto raii_mmaped_file::mmap_shared(raii_file &&file,
  150. int flags, std::int64_t offset) -> tl::expected<raii_mmaped_file, error>
  151. {
  152. void *map;
  153. if (file.get_stat().st_size < offset || offset < 0) {
  154. return tl::make_unexpected(error{
  155. fmt::format("cannot mmap file {} due to incorrect offset; offset={}, size={}",
  156. file.get_name(), offset, file.get_size()),
  157. EINVAL});
  158. }
  159. /* Update stat on file to ensure it is up-to-date */
  160. file.update_stat();
  161. map = mmap(nullptr, (std::size_t)(file.get_size() - offset), flags, MAP_SHARED, file.get_fd(), offset);
  162. if (map == MAP_FAILED) {
  163. return tl::make_unexpected(error{fmt::format("cannot mmap file {}: {}",
  164. file.get_name(), ::strerror(errno)),
  165. errno});
  166. }
  167. return raii_mmaped_file{std::move(file), map, (std::size_t)(file.get_size() - offset)};
  168. }
  169. auto raii_mmaped_file::mmap_shared(const char *fname, int open_flags,
  170. int mmap_flags, std::int64_t offset) -> tl::expected<raii_mmaped_file, error>
  171. {
  172. auto file = raii_file::open(fname, open_flags);
  173. if (!file.has_value()) {
  174. return tl::make_unexpected(file.error());
  175. }
  176. return raii_mmaped_file::mmap_shared(std::move(file.value()), mmap_flags, offset);
  177. }
  178. raii_mmaped_file::~raii_mmaped_file()
  179. {
  180. if (map != nullptr) {
  181. munmap(map, map_size);
  182. }
  183. }
  184. raii_mmaped_file::raii_mmaped_file(raii_mmaped_file &&other) noexcept
  185. : file(std::move(other.file))
  186. {
  187. std::swap(map, other.map);
  188. std::swap(map_size, other.map_size);
  189. }
  190. auto raii_file_sink::create(const char *fname, int flags, int perms,
  191. const char *suffix) -> tl::expected<raii_file_sink, error>
  192. {
  193. if (!fname || !suffix) {
  194. return tl::make_unexpected(error{"cannot create file; filename is nullptr", EINVAL, error_category::CRITICAL});
  195. }
  196. auto tmp_fname = fmt::format("{}.{}", fname, suffix);
  197. auto file = raii_locked_file::create(tmp_fname.c_str(), flags, perms);
  198. if (!file.has_value()) {
  199. return tl::make_unexpected(file.error());
  200. }
  201. return raii_file_sink{std::move(file.value()), fname, std::move(tmp_fname)};
  202. }
  203. auto raii_file_sink::write_output() -> bool
  204. {
  205. if (success) {
  206. /* We cannot write output twice */
  207. return false;
  208. }
  209. if (rename(tmp_fname.c_str(), output_fname.c_str()) == -1) {
  210. return false;
  211. }
  212. success = true;
  213. return true;
  214. }
  215. raii_file_sink::~raii_file_sink()
  216. {
  217. if (!success) {
  218. /* Unlink sink */
  219. unlink(tmp_fname.c_str());
  220. }
  221. }
  222. raii_file_sink::raii_file_sink(raii_locked_file &&_file, const char *_output, std::string &&_tmp_fname)
  223. : file(std::move(_file)), output_fname(_output), tmp_fname(std::move(_tmp_fname)), success(false)
  224. {
  225. }
  226. raii_file_sink::raii_file_sink(raii_file_sink &&other) noexcept
  227. : file(std::move(other.file)),
  228. output_fname(std::move(other.output_fname)),
  229. tmp_fname(std::move(other.tmp_fname)),
  230. success(other.success)
  231. {
  232. }
  233. namespace tests {
  234. template<class T>
  235. static auto test_read_file(const T &f)
  236. {
  237. auto fd = f.get_fd();
  238. (void) ::lseek(fd, 0, SEEK_SET);
  239. std::string buf('\0', (std::size_t) f.get_size());
  240. ::read(fd, buf.data(), buf.size());
  241. return buf;
  242. }
  243. template<class T>
  244. static auto test_write_file(const T &f, const std::string_view &buf)
  245. {
  246. auto fd = f.get_fd();
  247. (void) ::lseek(fd, 0, SEEK_SET);
  248. return ::write(fd, buf.data(), buf.size());
  249. }
  250. auto random_fname(std::string_view extension)
  251. {
  252. const auto *tmpdir = getenv("TMPDIR");
  253. if (tmpdir == nullptr) {
  254. tmpdir = G_DIR_SEPARATOR_S "tmp";
  255. }
  256. std::string out_fname{tmpdir};
  257. out_fname += G_DIR_SEPARATOR_S;
  258. char hexbuf[32];
  259. rspamd_random_hex(hexbuf, sizeof(hexbuf));
  260. out_fname.append((const char *) hexbuf, sizeof(hexbuf));
  261. if (!extension.empty()) {
  262. out_fname.append(".");
  263. out_fname.append(extension);
  264. }
  265. return out_fname;
  266. }
  267. TEST_SUITE("loked files utils")
  268. {
  269. TEST_CASE("create and delete file")
  270. {
  271. auto fname = random_fname("tmp");
  272. {
  273. auto raii_locked_file = raii_locked_file::create_temp(fname.c_str(), O_RDONLY, 00600);
  274. CHECK(raii_locked_file.has_value());
  275. CHECK(raii_locked_file.value().get_extension() == "tmp");
  276. CHECK(::access(fname.c_str(), R_OK) == 0);
  277. }
  278. // File must be deleted after this call
  279. auto ret = ::access(fname.c_str(), R_OK);
  280. auto serrno = errno;
  281. CHECK(ret == -1);
  282. CHECK(serrno == ENOENT);
  283. // Create one more time
  284. {
  285. auto raii_locked_file = raii_locked_file::create_temp(fname.c_str(), O_RDONLY, 00600);
  286. CHECK(raii_locked_file.has_value());
  287. CHECK(::access(fname.c_str(), R_OK) == 0);
  288. }
  289. ret = ::access(fname.c_str(), R_OK);
  290. serrno = errno;
  291. CHECK(ret == -1);
  292. CHECK(serrno == ENOENT);
  293. }
  294. TEST_CASE("check lock")
  295. {
  296. auto fname = random_fname("");
  297. {
  298. auto raii_locked_file = raii_locked_file::create_temp(fname.c_str(), O_RDONLY, 00600);
  299. CHECK(raii_locked_file.has_value());
  300. CHECK(raii_locked_file.value().get_extension() == "");
  301. CHECK(::access(fname.c_str(), R_OK) == 0);
  302. auto raii_locked_file2 = raii_locked_file::open(fname.c_str(), O_RDONLY);
  303. CHECK(!raii_locked_file2.has_value());
  304. CHECK(::access(fname.c_str(), R_OK) == 0);
  305. }
  306. // File must be deleted after this call
  307. auto ret = ::access(fname.c_str(), R_OK);
  308. auto serrno = errno;
  309. CHECK(ret == -1);
  310. CHECK(serrno == ENOENT);
  311. }
  312. auto get_tmpdir()->std::string
  313. {
  314. const auto *tmpdir = getenv("TMPDIR");
  315. if (tmpdir == nullptr) {
  316. tmpdir = G_DIR_SEPARATOR_S "tmp";
  317. }
  318. std::size_t sz;
  319. std::string mut_fname = tmpdir;
  320. rspamd_normalize_path_inplace(mut_fname.data(), mut_fname.size(), &sz);
  321. mut_fname.resize(sz);
  322. if (!mut_fname.ends_with(G_DIR_SEPARATOR)) {
  323. mut_fname += G_DIR_SEPARATOR;
  324. }
  325. return mut_fname;
  326. }
  327. TEST_CASE("tempfile")
  328. {
  329. std::string tmpname;
  330. const std::string tmpdir{get_tmpdir()};
  331. {
  332. auto raii_locked_file = raii_locked_file::mkstemp(std::string(tmpdir + G_DIR_SEPARATOR_S + "doctest-XXXXXXXX").c_str(),
  333. O_RDONLY, 00600);
  334. CHECK(raii_locked_file.has_value());
  335. CHECK(raii_locked_file.value().get_dir() == tmpdir);
  336. CHECK(access(raii_locked_file.value().get_name().data(), R_OK) == 0);
  337. auto raii_locked_file2 = raii_locked_file::open(raii_locked_file.value().get_name().data(), O_RDONLY);
  338. CHECK(!raii_locked_file2.has_value());
  339. CHECK(access(raii_locked_file.value().get_name().data(), R_OK) == 0);
  340. tmpname = raii_locked_file.value().get_name();
  341. }
  342. // File must be deleted after this call
  343. auto ret = ::access(tmpname.c_str(), R_OK);
  344. auto serrno = errno;
  345. CHECK(ret == -1);
  346. CHECK(serrno == ENOENT);
  347. }
  348. TEST_CASE("mmap")
  349. {
  350. std::string tmpname;
  351. const std::string tmpdir{get_tmpdir()};
  352. {
  353. auto raii_file = raii_file::mkstemp(std::string(tmpdir + G_DIR_SEPARATOR_S + "doctest-XXXXXXXX").c_str(),
  354. O_RDWR | O_CREAT | O_EXCL, 00600);
  355. CHECK(raii_file.has_value());
  356. CHECK(raii_file->get_dir() == tmpdir);
  357. CHECK(access(raii_file->get_name().data(), R_OK) == 0);
  358. tmpname = std::string{raii_file->get_name()};
  359. char payload[] = {'1', '2', '3'};
  360. CHECK(write(raii_file->get_fd(), payload, sizeof(payload)) == sizeof(payload));
  361. auto mmapped_file1 = raii_mmaped_file::mmap_shared(std::move(raii_file.value()), PROT_READ | PROT_WRITE);
  362. CHECK(mmapped_file1.has_value());
  363. CHECK(!raii_file->is_valid());
  364. CHECK(mmapped_file1->get_size() == sizeof(payload));
  365. CHECK(memcmp(mmapped_file1->get_map(), payload, sizeof(payload)) == 0);
  366. *(char *) mmapped_file1->get_map() = '2';
  367. auto mmapped_file2 = raii_mmaped_file::mmap_shared(tmpname.c_str(), O_RDONLY, PROT_READ);
  368. CHECK(mmapped_file2.has_value());
  369. CHECK(mmapped_file2->get_size() == sizeof(payload));
  370. CHECK(memcmp(mmapped_file2->get_map(), payload, sizeof(payload)) != 0);
  371. CHECK(memcmp(mmapped_file2->get_map(), mmapped_file1->get_map(), sizeof(payload)) == 0);
  372. }
  373. // File must be deleted after this call
  374. auto ret = ::access(tmpname.c_str(), R_OK);
  375. auto serrno = errno;
  376. CHECK(ret == -1);
  377. CHECK(serrno == ENOENT);
  378. }
  379. }// TEST_SUITE
  380. }// namespace tests
  381. }// namespace rspamd::util