From 47042b36323a9cc5463f7b20be9373421f681aa6 Mon Sep 17 00:00:00 2001 From: Yifan Yuan Date: Sat, 24 Oct 2026 22:38:44 +0800 Subject: [PATCH] Optimize the ext4 data layout in userspace conversion overlaybd-apply wrote regular files through ext4, so in a LSMT layer the file data was interleaved with the fs metadata that ext4 wrote back whenever its cache flushed, scattering fsmeta all over the data area. A plain LSMT layer is now converted in two pas/ses. The tar stream is read once, appending the padded data of every regular file to the upper data file in tar order and spooling the headers to a temporary file, with each file's data offset recorded in devmajor. Then the ext4 fs is created and the headers are replayed metadata-only, as TurboOCI does, mapping each file's blocks to its staged data through RemoteData. The mappings are applied after the fs is synced, so a metadata block reused for file data cannot be written back over it, and the tail of a reused last block is zeroed. `overlaybd-commit --seal` then appends the index and trailer in place, giving Header | file data | fs metadata | index | Trailer Signed-off-by: Yifan Yuan --- CMake/Findphoton.cmake | 5 + docs/README.md | 12 + docs/apply-data-layout.md | 58 ++ docs/apply-data-layout_zh.md | 58 ++ src/overlaybd/lsmt/file.cpp | 75 ++- src/overlaybd/lsmt/file.h | 3 + src/overlaybd/lsmt/test/test.cpp | 320 +++++++++++ src/overlaybd/tar/fsmeta_file.cpp | 196 +++++++ src/overlaybd/tar/fsmeta_file.h | 57 ++ src/overlaybd/tar/libtar.cpp | 253 ++++++++- src/overlaybd/tar/libtar.h | 17 +- src/overlaybd/tar/test/test.cpp | 866 +++++++++++++++++++++++++++++- src/overlaybd/tar/whiteout.cpp | 2 +- src/tools/overlaybd-apply.cpp | 84 ++- 14 files changed, 1952 insertions(+), 54 deletions(-) create mode 100644 docs/apply-data-layout.md create mode 100644 docs/apply-data-layout_zh.md create mode 100644 src/overlaybd/tar/fsmeta_file.cpp create mode 100644 src/overlaybd/tar/fsmeta_file.h diff --git a/CMake/Findphoton.cmake b/CMake/Findphoton.cmake index 3a0dc014..91f80cb2 100644 --- a/CMake/Findphoton.cmake +++ b/CMake/Findphoton.cmake @@ -3,6 +3,8 @@ set(FETCHCONTENT_QUIET false) set(PHOTON_ENABLE_EXTFS ON CACHE BOOL "Build Photon extfs support" FORCE) set(PHOTON_BUILD_OCF_CACHE ON CACHE BOOL "Build Photon OCF cache support" FORCE) set(PHOTON_ENABLE_URING ON CACHE BOOL "Build Photon io_uring support" FORCE) +# Photon must link the liburing ublksrv uses (Finduring.cmake), never a private copy. +set(PHOTON_URING_SOURCE "" CACHE STRING "Photon's own liburing source, unused" FORCE) if(NOT ORIGIN_EXT2FS) set(PHOTON_ENABLE_RESIZE ON CACHE BOOL "Build Photon extfs resize support" FORCE) @@ -70,4 +72,7 @@ if(NOT ORIGIN_EXT2FS) add_dependencies(photon_obj libext2fs) endif() +# The io_uring wrapper includes headers that liburing's configure generates. +add_dependencies(photon_obj liburing_build) + set(PHOTON_INCLUDE_DIR ${photon_SOURCE_DIR}/include/) diff --git a/docs/README.md b/docs/README.md index c4d5a170..01068c26 100644 --- a/docs/README.md +++ b/docs/README.md @@ -70,6 +70,18 @@ Overlaybd achieves fast container startup through the following key designs: without copy-up. Actually writes to overlaybd are pure writes - no lower-layer data is ever copied on write. ~~CoW~~ +- **Userspace conversion**: overlaybd integrates + [erofs-utils](https://github.com/erofs/erofs-utils) and + [e2fsprogs](https://github.com/tytso/e2fsprogs) to build **erofs** and **ext4** layers entirely in + userspace — an OCI tar layer is turned into a block layer directly, with no kernel mount and + no loop device in the conversion path. + + **For ext4 the layout is optimized further**, all file data is placed ahead of the ext4 metadata, + so metadata reads concentrate in a small region at the end of the layer instead of being scattered + across it. On a 182-layer benchmark this cuts the unique 1 MiB coverage of a metadata-heavy + workload by over 95%, which directly reduces how much data on-demand fetching has to pull. + See [data-layout](apply-data-layout.md) for details. + - **Seekable compression (ZFile)**: Random reads decompress only the requested data range, not the entire compressed block. Because compressed data is smaller, the actual I/O transfer is reduced enough that compressed random reads are often *faster* than diff --git a/docs/apply-data-layout.md b/docs/apply-data-layout.md new file mode 100644 index 00000000..26fcdc06 --- /dev/null +++ b/docs/apply-data-layout.md @@ -0,0 +1,58 @@ +# overlaybd Data Layout Optimization (EXT4) + +[简体中文](apply-data-layout_zh.md) + +`overlaybd-apply` converts an OCI tar layer into an overlaybd layer via **e2fsprogs**. Rearranging where the file data payload and the ext4 metadata land inside the layer substantially improves the locality of metadata access. + +## New layout + +```text +old: | Header | data | fsmeta | data | fsmeta copy | data | ... | index | Trailer | + +new: | Header 4 KiB | payload: 4 KiB aligned per file | fsmeta: live metadata | index | Trailer 4 KiB | + [4096, payload_end) [payload_end, index_offset) +``` + +- **payload**: the data of every regular file, staged in tar order; each non-empty file starts at a 4 KiB aligned offset and occupies a 4 KiB aligned length with the tail zeroed. Empty files take no space. +- **fsmeta**: the metadata blocks that are live at the end, appended compactly in ascending logical order, with no duplicate copies. +- **index / Trailer**: appended in place by `overlaybd-commit --seal`; the data is never rearranged. + +The result is still a standard sealed LSMT — no new format version, no new tag, no external payload file. + +## How it works + +```text +tar stream → append payload sequentially (aligned per file, no mapping yet) + temp tar headers (recording payload offsets) +temp headers → ext4 replays metadata only → FsMetaFile (sparse file, original logical offsets) + └─ regular files: fallocate + FIEMAP → pending ext4-block-to-payload mappings +close ext4 → drop payload blocks → append live fsmeta compactly → install full-block payload mappings +commit --seal → merge adjacent mappings, append index and Trailer in place +``` + +## Measured results + +Converted 10 SWE-benchmark images into overlaybd images with a 64GB block size, 182 layers in total. After mounting, `du`/`find` were run to collect and compare the distribution of accessed ranges between the old and the new layout: + +| Metric (du+find, MB is decimal) | Old | New | Change | +|---|---:|---:|---:| +| LSMT requested MB | 1949.475 | 1948.631 | nearly unchanged | +| LSMT bytes in the second half | 63.20% | 99.92% | more concentrated | +| LSMT position p5–p95 span | 90.82% | 6.38% | more concentrated | +| LSMT 1 MiB blocks touched | 12851 | 1276 | −90.07% | +| ZFile local requested MB | 110.352 | 103.290 | −6.40% | +| ZFile unique 4 KiB coverage MB | 436.945 | 56.717 | −87.02% | +| ZFile unique 1 MiB coverage MB | 6299.222 | 271.553 | −95.69% | +| Sealed total MB | 44574.161 | 32020.957 | −28.16% | +| ZFile total MB | 17818.462 | 17192.135 | −3.52% | + +- **Access is concentrated**: in the new layout 99.95% of the R requested bytes fall in the fsmeta region, which is only 5.80% of the data area; in compressed space the range corresponding to fsmeta accounts for 99.45% of the bytes read. +- **Coverage shrinks sharply at coarser fetch granularities**: requested bytes drop by only 6.40%, while unique coverage aligned to 4 KiB / 1 MiB drops by 87.02% / 95.69%. + +## Related code + +| File | Contents | +|---|---| +| `src/tools/overlaybd-apply.cpp` | path selection and orchestration | +| `src/overlaybd/tar/libtar.cpp` | stage_tar, extract_staged, apply_pending; per-file alignment and zeroed tails | +| `src/overlaybd/tar/fsmeta_file.{h,cpp}` | sparse fsmeta staging, compact append of live blocks | +| `src/overlaybd/lsmt/file.cpp` | staged RemoteData mappings, close_seal and index merging | diff --git a/docs/apply-data-layout_zh.md b/docs/apply-data-layout_zh.md new file mode 100644 index 00000000..b57754dc --- /dev/null +++ b/docs/apply-data-layout_zh.md @@ -0,0 +1,58 @@ +# overlaybd 数据布局优化(EXT4) + +[English](apply-data-layout.md) + +`overlaybd-apply` 通过 **e2fsprogs** 把 OCI tar 层转换为 overlaybd 层, 通过调整文件数据 payload 和 ext4 元数据在layer中的布局,可有效改善元数据访问的局部性。 + +## 新布局 + +```text +旧:| Header | 数据 | fsmeta | 数据 | fsmeta 副本 | 数据 | ... | index | Trailer | + +新:| Header 4 KiB | payload:逐文件 4 KiB 对齐 | fsmeta:有效元数据 | index | Trailer 4 KiB | + [4096, payload_end) [payload_end, index_offset) +``` + +- **payload**:按 tar 顺序暂存所有普通文件的数据;每个非空文件起点和占用长度均按 4 KiB 对齐,尾部补零,空文件不占空间。 +- **fsmeta**:最终有效的元数据块,按逻辑地址升序紧凑追加,无重复副本。 +- **index / Trailer**:`overlaybd-commit --seal` 原地追加,数据不重排。 + +结果仍是标准 sealed LSMT,没有新格式版本、tag 或外部 payload 文件。 + +## 基本原理 + +```text +tar 流 → 顺序追加 payload(逐文件对齐,不建映射)+ 临时 tar 头部(记录 payload 偏移) +临时头部 → ext4 只回放元数据 → FsMetaFile(稀疏文件,原逻辑偏移) + └─ 普通文件:fallocate + FIEMAP → 待安装 ext4 块到 payload 的映射 +关闭 ext4 → 剔除 payload 块 → 有效 fsmeta 紧凑追加 → 安装完整块 payload 映射 +commit --seal → 合并相邻映射,原地追加 index 和 Trailer +``` + +## 收益实测 + +将 10 个 SWE评测镜像转换成大小为块大小为 64GB 的 overlaybd 镜像,共 182 层。挂载后跑 du/find,统计并对比新旧布局的访问区间分布: + +| 指标(du+find,MB 为十进制) | 旧版 | 新版 | 变化 | +|---|---:|---:|---:| +| LSMT 请求 MB | 1949.475 | 1948.631 | 近乎不变 | +| LSMT 后半段字节占比 | 63.20% | 99.92% | 更集中 | +| LSMT 位置 p5–p95 跨度 | 90.82% | 6.38% | 更集中 | +| LSMT 触及 1 MiB 块 | 12851 | 1276 | −90.07% | +| ZFile 本地请求 MB | 110.352 | 103.290 | −6.40% | +| ZFile 唯一 4 KiB 覆盖 MB | 436.945 | 56.717 | −87.02% | +| ZFile 唯一 1 MiB 覆盖 MB | 6299.222 | 271.553 | −95.69% | +| sealed 合计 MB | 44574.161 | 32020.957 | −28.16% | +| ZFile 合计 MB | 17818.462 | 17192.135 | −3.52% | + +- **访问集中**:新版 99.95% 的 R 请求字节落在 fsmeta 区,该区只占数据区 5.80%;压缩空间中 fsmeta 对应区间占读取字节 99.45%。 +- **较粗回填粒度的覆盖明显减少**:请求字节仅下降 6.40%,对齐 4 KiB/1 MiB 的唯一覆盖分别下降 87.02%/95.69%。 + +## 相关代码 + +| 文件 | 内容 | +|---|---| +| `src/tools/overlaybd-apply.cpp` | 流程选择与编排 | +| `src/overlaybd/tar/libtar.cpp` | stage_tar、extract_staged、apply_pending;逐文件对齐与零尾 | +| `src/overlaybd/tar/fsmeta_file.{h,cpp}` | 稀疏 fsmeta 暂存、有效块紧凑追加 | +| `src/overlaybd/lsmt/file.cpp` | staged RemoteData 映射、close_seal 与索引合并 | diff --git a/src/overlaybd/lsmt/file.cpp b/src/overlaybd/lsmt/file.cpp index 78d65317..747392a5 100644 --- a/src/overlaybd/lsmt/file.cpp +++ b/src/overlaybd/lsmt/file.cpp @@ -774,6 +774,7 @@ class LSMTReadOnlyFile : public IFileRW { } UNIMPLEMENTED(int restack(IFileRW *upper) override); + UNIMPLEMENTED(int can_map_staged_data() const override); }; // The append-only mapping log in the index file of a writable layer. A mapping is @@ -977,6 +978,9 @@ class LSMTFile : public LSMTReadOnlyFile { } return 0; } + if (request == RemoteData) { + return map_staged_data(va_arg(args, RemoteMapping)); + } if (request != Index_Group_Commit) LOG_ERROR_RETURN(EINVAL, -1, "invaid request code"); @@ -984,6 +988,52 @@ class LSMTFile : public LSMTReadOnlyFile { return m_log ? m_log->set_group_commit(buffer_size) : 0; } + virtual int can_map_staged_data() const override { + if (m_rw_type == RWType::Append) + return 0; + errno = ENOTSUP; + return -1; + } + + // maps [offset, +count) of the virtual file to moffset of the data file of tag + int insert_mappings(off_t offset, uint32_t count, off_t moffset, uint8_t tag) { + if (offset < 0 || moffset < 0 || !is_aligned((uint64_t)offset | count | (uint64_t)moffset)) + LOG_ERROR_RETURN(EINVAL, -1, "unaligned mapping {offset: `, count: `, moffset: `}", + offset, count, moffset); + size_t nwrite = 0; + while (count > 0) { + auto length = min(count / ALIGNMENT, (uint32_t)Segment::MAX_LENGTH); + SegmentMapping m((uint64_t)offset / ALIGNMENT, length, + (uint64_t)moffset / ALIGNMENT, tag); + LOG_DEBUG("insert segment: `", m); + if (commit_mapping(m, [&] { index_insert(m); }) != 0) + return -1; + auto bytes = length * ALIGNMENT; + nwrite += bytes; + offset += bytes; + count -= bytes; + moffset += bytes; + } + return nwrite; + } + + // maps [lba.offset, +lba.count) of the virtual file to the data staged at lba.roffset + int map_staged_data(RemoteMapping lba) { + if (can_map_staged_data() != 0) + LOG_ERRNO_RETURN(0, -1, "the layer cannot map staged data"); + Lock lock(m_rw_mtx); + struct stat st; + if (m_files[m_rw_tag]->fstat(&st) != 0) + LOG_ERRNO_RETURN(0, -1, "failed to stat the upper data file"); + if (lba.roffset < (off_t)HeaderTrailer::SPACE || lba.roffset > st.st_size || + lba.count > (uint64_t)(st.st_size - lba.roffset)) + LOG_ERROR_RETURN(EINVAL, -1, + "mapping {offset: `, count: `, roffset: `} out of data [`, `)", + lba.offset, lba.count, lba.roffset, (uint64_t)HeaderTrailer::SPACE, + st.st_size); + return insert_mappings(lba.offset, lba.count, lba.roffset, m_rw_tag); + } + virtual int init_concurrency() { if (m_init_concurrency) return 0; @@ -1241,9 +1291,12 @@ class LSMTFile : public LSMTReadOnlyFile { mapping.reset(m_index0->dump(ALIGNMENT)); index_size = m_index0->size(); } + index_size = compress_raw_index(mapping.get(), index_size); uint64_t index_offset = m_files[m_rw_tag]->lseek(0, SEEK_END); ssize_t index_bytes = index_size * sizeof(SegmentMapping); index_bytes = (index_bytes + ALIGNMENT - 1) / ALIGNMENT * ALIGNMENT; + std::fill(mapping.get() + index_size, mapping.get() + index_bytes / sizeof(SegmentMapping), + SegmentMapping::invalid_mapping()); auto ret = m_files[m_rw_tag]->pwrite(mapping.get(), index_bytes, index_offset); if (ret < index_bytes) LOG_ERRNO_RETURN(0, -1, "failed to write index."); @@ -1484,6 +1537,7 @@ class LSMTSparseFile : public LSMTFile { return 0; } + UNIMPLEMENTED(int can_map_staged_data() const override); }; class LSMTWarpFile : public LSMTFile { @@ -1530,25 +1584,12 @@ class LSMTWarpFile : public LSMTFile { va_end(tmp); LOG_DEBUG("RemoteMapping: {offset: `, count: `, roffset: `}", lba.offset, lba.count, lba.roffset); - size_t nwrite = 0; - while (lba.count > 0) { - SegmentMapping m; - m.offset = lba.offset / ALIGNMENT; - m.length = (Segment::MAX_LENGTH < lba.count / ALIGNMENT ? Segment::MAX_LENGTH - : lba.count / ALIGNMENT); - m.moffset = lba.roffset / ALIGNMENT; - m.tag = m_rw_tag + (uint8_t)SegmentType::remoteData; - LOG_DEBUG("insert segment: `", m); - if (commit_mapping(m, [&] { index_insert(m); }) != 0) - return -1; - nwrite += m.length * ALIGNMENT; - lba.offset += m.length * ALIGNMENT; - lba.count -= m.length * ALIGNMENT; - lba.roffset += m.length * ALIGNMENT; - } - return nwrite; + return insert_mappings(lba.offset, lba.count, lba.roffset, + m_rw_tag + (uint8_t)SegmentType::remoteData); } + UNIMPLEMENTED(int can_map_staged_data() const override); + size_t compact(CompactOptions &opts, size_t moffset, size_t &nindex) const { auto dest_file = opts.commit_args->as; diff --git a/src/overlaybd/lsmt/file.h b/src/overlaybd/lsmt/file.h index fee4e57c..90f8b000 100644 --- a/src/overlaybd/lsmt/file.h +++ b/src/overlaybd/lsmt/file.h @@ -115,6 +115,9 @@ class IFileRW : public IFileRO { // close_seal current RW layer (change it to RO layer) and re-stack with upper layer virtual int restack(IFileRW *upper) = 0; + + // returns 0 if RemoteData can map the data appended to the top data file, -1 otherwise + virtual int can_map_staged_data() const = 0; }; enum class RWType : uint8_t { diff --git a/src/overlaybd/lsmt/test/test.cpp b/src/overlaybd/lsmt/test/test.cpp index 454a1e4d..938fc27d 100644 --- a/src/overlaybd/lsmt/test/test.cpp +++ b/src/overlaybd/lsmt/test/test.cpp @@ -2030,6 +2030,326 @@ TEST_F(WarpFileTest, commit_with_uuid) { EXPECT_EQ(uu, uu_commit); } +class StagingTest : public FileTest { +public: + struct FreeDeleter { + void operator()(char *p) const { + free(p); + } + }; + using Buffer = unique_ptr; + + // the expected content of the virtual file + vector expected; + + static Buffer pattern(size_t size, uint32_t seed) { + void *p = nullptr; + EXPECT_EQ(0, posix_memalign(&p, ALIGNMENT4K, size)); + auto x = seed * 2654435761u + 1; + for (size_t i = 0; i < size; i++) { + x = x * 1103515245 + 12345; + ((char *)p)[i] = x >> 16; + } + return Buffer((char *)p); + } + + static RemoteMapping mapping(off_t offset, uint32_t count, off_t roffset) { + RemoteMapping m; + m.offset = offset; + m.count = count; + m.roffset = roffset; + return m; + } + + void expect(off_t offset, const char *data, size_t count) { + if (expected.size() < offset + count) + expected.resize(offset + count, 0); + memcpy(&expected[offset], data, count); + } + + // `data` is what the target holds at `roffset` + void map(IFileRW *file, off_t offset, uint32_t count, off_t roffset, const char *data) { + ASSERT_EQ((int)count, file->ioctl(IFileRW::RemoteData, mapping(offset, count, roffset))); + expect(offset, data, count); + } + + void write(IFileRW *file, off_t offset, const char *data, size_t count) { + ASSERT_EQ((ssize_t)count, file->pwrite(data, count, offset)); + expect(offset, data, count); + } + + static off_t append_payload(IFileRW *file, const char *data, size_t count) { + auto upper = file->get_lower_files().back(); + auto pos = upper->lseek(0, SEEK_END); + EXPECT_GE(pos, (off_t)HeaderTrailer::SPACE); + EXPECT_EQ((ssize_t)count, upper->pwrite(data, count, pos)); + return pos; + } + + void expect_rejected(IFileRW *file, off_t offset, uint32_t count, off_t roffset, int err) { + errno = 0; + EXPECT_EQ(-1, file->ioctl(IFileRW::RemoteData, mapping(offset, count, roffset))); + EXPECT_EQ(err, errno); + } + + void verify(IFileRO *file) { + auto readback = pattern(expected.size(), 0); + ASSERT_EQ((ssize_t)expected.size(), file->pread(readback.get(), expected.size(), 0)); + EXPECT_EQ(0, memcmp(expected.data(), readback.get(), expected.size())); + } + + // the index of a sealed layer as it is stored in the layer + vector sealed_index(const char *fn, HeaderTrailer &trailer) { + unique_ptr file(lfs->open(fn, O_RDONLY)); + ALIGNED_MEM(buf, HeaderTrailer::SPACE, ALIGNMENT4K); + auto pht = verify_ht(file.get(), buf, true); + if (pht == nullptr) { + ADD_FAILURE() << "no valid trailer in " << fn; + return {}; + } + trailer = *pht; + vector index(pht->index_size); + auto bytes = index.size() * sizeof(SegmentMapping); + EXPECT_EQ((ssize_t)bytes, file->pread(index.data(), bytes, pht->index_offset)); + return index; + } +}; + +// staged data stays unmapped until RemoteData maps it, then survives reopen and close_seal() +TEST_F(StagingTest, stage_and_map) { + auto file = create_file_rw(); + ASSERT_NE(nullptr, file); + ASSERT_EQ(0, file->can_map_staged_data()); + auto index_size = file_size(lfs, idx_name.back().c_str()); + + // the first payload needs more than one mapping + const size_t N1 = (Segment::MAX_LENGTH + 100) * ALIGNMENT, N2 = 64 << 10; + auto a = pattern(N1, 1), b = pattern(N2, 2), c = pattern(32 << 10, 3); + auto pa = append_payload(file, a.get(), N1); + auto pb = append_payload(file, b.get(), N2); + EXPECT_EQ((off_t)HeaderTrailer::SPACE, pa); + EXPECT_EQ(pa + (off_t)N1, pb); + EXPECT_EQ(pb + (ssize_t)N2, file_size(lfs, data_name.back().c_str())); + EXPECT_EQ(0UL, file->index()->size()); + EXPECT_EQ(index_size, file_size(lfs, idx_name.back().c_str())); + + expect_rejected(file, (1 << 20) + 1, ALIGNMENT, pa, EINVAL); + expect_rejected(file, 1 << 20, 100, pa, EINVAL); + expect_rejected(file, 1 << 20, ALIGNMENT, pa + 1, EINVAL); + expect_rejected(file, -(off_t)ALIGNMENT, ALIGNMENT, pa, EINVAL); + expect_rejected(file, 0, ALIGNMENT, -(off_t)ALIGNMENT, EINVAL); + expect_rejected(file, 0, ALIGNMENT, 0, EINVAL); + expect_rejected(file, 0, ALIGNMENT, pa - ALIGNMENT, EINVAL); + expect_rejected(file, 0, 2 * ALIGNMENT, pb + N2 - ALIGNMENT, EINVAL); + expect_rejected(file, 0, ALIGNMENT, pb + N2, EINVAL); + expect_rejected(file, 0, ALIGNMENT, INT64_MAX & ~(off_t)(ALIGNMENT - 1), EINVAL); + EXPECT_EQ(pb + (ssize_t)N2, file_size(lfs, data_name.back().c_str())); + EXPECT_EQ(0UL, file->index()->size()); + EXPECT_EQ(index_size, file_size(lfs, idx_name.back().c_str())); + + map(file, 1 << 20, N1, pa, a.get()); + EXPECT_EQ(2UL, file->index()->size()); + map(file, 0, N2, pb, b.get()); + // overrides a part of the first payload + map(file, (1 << 20) + 4096, 8192, pb, b.get()); + EXPECT_EQ(index_size + 4 * (ssize_t)sizeof(SegmentMapping), + file_size(lfs, idx_name.back().c_str())); + + write(file, 12 << 20, c.get(), 32 << 10); + SegmentMapping written; + ASSERT_EQ(1UL, file->index()->lookup(Segment{(12 << 20) / ALIGNMENT, 1}, &written, 1)); + EXPECT_EQ((uint64_t)(pb + N2), written.moffset * ALIGNMENT); + map(file, 24 << 20, ALIGNMENT, pb + N2, c.get()); + verify(file); + delete file; + + file = open_file_rw(); + ASSERT_NE(nullptr, file); + verify(file); + ASSERT_EQ(0, file->can_map_staged_data()); + map(file, 0, ALIGNMENT, pa, a.get()); + auto d = pattern(4096, 4); + auto pd = append_payload(file, d.get(), 4096); + EXPECT_EQ(pb + (off_t)N2 + (32 << 10), pd); + map(file, 20 << 20, 4096, pd, d.get()); + verify(file); + auto data_end = file_size(lfs, data_name.back().c_str()); + EXPECT_EQ(pd + 4096, data_end); + + IFileRO *sealed = nullptr; + ASSERT_EQ(0, file->close_seal(&sealed)); + delete file; + ASSERT_NE(nullptr, sealed); + verify(sealed); + delete sealed; + + // close_seal() appends the index and the trailer after the data in place + HeaderTrailer ht; + auto index = sealed_index(data_name.back().c_str(), ht); + EXPECT_EQ((uint64_t)data_end, ht.index_offset); + auto index_bytes = (index.size() * sizeof(SegmentMapping) + ALIGNMENT - 1) / ALIGNMENT * ALIGNMENT; + EXPECT_EQ(data_end + (ssize_t)(index_bytes + HeaderTrailer::SPACE), + file_size(lfs, data_name.back().c_str())); + uint64_t end = 0; + for (auto &m : index) { + EXPECT_EQ(0, (int)m.tag); + EXPECT_FALSE(m.zeroed); + EXPECT_LE(end, (uint64_t)m.offset); + end = m.end(); + EXPECT_LE((uint64_t)pa, m.moffset * ALIGNMENT); + EXPECT_LE(m.mend() * ALIGNMENT, (uint64_t)data_end); + } + + auto ro = open_file_ro(data_name.back().c_str()); + ASSERT_NE(nullptr, ro); + verify(ro); + delete ro; +} + +// staging into a layer stacked on a sealed lower layer only touches the upper layer +TEST_F(StagingTest, stacked_lower) { + auto lower_rw = create_file_rw(); + ASSERT_NE(nullptr, lower_rw); + auto l = pattern(256 << 10, 5); + write(lower_rw, 0, l.get(), 256 << 10); + IFileRO *lower = nullptr; + ASSERT_EQ(0, lower_rw->close_seal(&lower)); + delete lower_rw; + auto lower_size = file_size(lfs, data_name.front().c_str()); + + auto upper = create_file_rw(); + ASSERT_NE(nullptr, upper); + auto file = stack_files(upper, lower, /* ownership = */ true, /* check_order = */ false); + ASSERT_NE(nullptr, file); + ASSERT_EQ(0, file->can_map_staged_data()); + auto u = pattern(64 << 10, 6); + auto pu = append_payload(file, u.get(), 64 << 10); + EXPECT_EQ((off_t)HeaderTrailer::SPACE, pu); + map(file, 64 << 10, 32 << 10, pu + (32 << 10), u.get() + (32 << 10)); + verify(file); + EXPECT_EQ(lower_size, file_size(lfs, data_name.front().c_str())); + EXPECT_EQ(pu + (64 << 10), file_size(lfs, data_name.back().c_str())); + delete file; + + // seal the upper layer alone, as `overlaybd-commit --seal` does + file = open_file_rw(); + ASSERT_NE(nullptr, file); + ASSERT_EQ(0, file->close_seal()); + delete file; + HeaderTrailer ht; + auto index = sealed_index(data_name.back().c_str(), ht); + ASSERT_EQ(1UL, index.size()); + EXPECT_EQ(128UL, (uint64_t)index[0].offset); + EXPECT_EQ(64U, (uint32_t)index[0].length); + EXPECT_EQ((uint64_t)(pu + (32 << 10)) / ALIGNMENT, (uint64_t)index[0].moffset); + EXPECT_EQ(0, (int)index[0].tag); + + IFile *files[] = {lfs->open(data_name.front().c_str(), O_RDONLY), + lfs->open(data_name.back().c_str(), O_RDONLY)}; + auto image = open_files_ro(files, 2, true); + ASSERT_NE(nullptr, image); + verify(image); + delete image; +} + +// close_seal() merges the mappings that are contiguous both in the virtual file and in the layer +TEST_F(StagingTest, close_seal_merges_mappings) { + auto file = create_file_rw(); + ASSERT_NE(nullptr, file); + const uint32_t N = 64 << 10; + auto a = pattern(3 * N, 8), w = pattern(2 * N, 9); + auto pa = append_payload(file, a.get(), 3 * N); + for (uint32_t i = 0; i < 3; i++) + map(file, (1 << 20) + i * N, N, pa + i * N, a.get() + i * N); + // logically adjacent to the payload, but not contiguous with it in the layer + map(file, (1 << 20) + 3 * N, N, pa, a.get()); + // contiguous with the payload in the layer, but not logically adjacent to it + write(file, 8 << 20, w.get(), N); + write(file, (8 << 20) + N, w.get() + N, N); + ASSERT_EQ(0, file->fallocate(3, 12 << 20, N)); + ASSERT_EQ(0, file->fallocate(3, (12 << 20) + N, N)); + vector zeros(2 * N, 0); + expect(12 << 20, zeros.data(), 2 * N); + EXPECT_EQ(8UL, file->index()->size()); + + IFileRO *sealed = nullptr; + ASSERT_EQ(0, file->close_seal(&sealed)); + delete file; + ASSERT_NE(nullptr, sealed); + EXPECT_EQ(4UL, sealed->index()->size()); + verify(sealed); + delete sealed; + + HeaderTrailer ht; + auto index = sealed_index(data_name.back().c_str(), ht); + ASSERT_EQ(4UL, index.size()); + auto expect_mapping = [](const SegmentMapping &m, off_t offset, uint32_t length, + off_t moffset, bool zeroed) { + EXPECT_EQ((uint64_t)offset / ALIGNMENT, (uint64_t)m.offset); + EXPECT_EQ(length / ALIGNMENT, (uint32_t)m.length); + EXPECT_EQ((uint64_t)moffset / ALIGNMENT, (uint64_t)m.moffset); + EXPECT_EQ(zeroed, (bool)m.zeroed); + }; + expect_mapping(index[0], 1 << 20, 3 * N, pa, false); + expect_mapping(index[1], (1 << 20) + 3 * N, N, pa, false); + expect_mapping(index[2], 8 << 20, 2 * N, pa + 3 * N, false); + expect_mapping(index[3], 12 << 20, 2 * N, pa + 5 * N, true); + // the rest of the last index block maps nothing + vector padding(ALIGNMENT / sizeof(SegmentMapping) - index.size()); + unique_ptr fdata(lfs->open(data_name.back().c_str(), O_RDONLY)); + auto padding_bytes = padding.size() * sizeof(SegmentMapping); + ASSERT_EQ((ssize_t)padding_bytes, + fdata->pread(padding.data(), padding_bytes, + ht.index_offset + index.size() * sizeof(SegmentMapping))); + for (auto &m : padding) + EXPECT_EQ((uint64_t)SegmentMapping::INVALID_OFFSET, (uint64_t)m.offset); + + auto ro = open_file_ro(data_name.back().c_str()); + ASSERT_NE(nullptr, ro); + EXPECT_EQ(4UL, ro->index()->size()); + verify(ro); + delete ro; +} + +// only a plain append-mode layer maps embedded payload, while warp keeps external RemoteData +TEST_F(StagingTest, other_layer_types) { + auto data = pattern(64 << 10, 7); + for (auto rw_type : {RWType::Sparse, RWType::Hybrid}) { + auto file = create_file_rw(rw_type); + ASSERT_NE(nullptr, file); + DEFER(delete file); + auto data_size = file_size(lfs, data_name.back().c_str()); + // the sparse layer leaves it unimplemented, the hybrid one does not support it + auto err = rw_type == RWType::Sparse ? ENOSYS : ENOTSUP; + errno = 0; + EXPECT_EQ(-1, file->can_map_staged_data()); + EXPECT_EQ(err, errno); + expect_rejected(file, 0, 4096, HeaderTrailer::SPACE, err); + EXPECT_EQ(data_size, file_size(lfs, data_name.back().c_str())); + EXPECT_EQ(0UL, file->index()->size()); + } + + name_next_layer(); + auto ftarget = lfs->open(layer_name.back().c_str(), O_RDWR | O_CREAT | O_TRUNC, S_IRWXU); + ASSERT_NE(nullptr, ftarget); + ASSERT_EQ((ssize_t)(64 << 10), ftarget->pwrite(data.get(), 64 << 10, 0)); + auto fmeta = lfs->open(data_name.back().c_str(), O_RDWR | O_CREAT | O_TRUNC, S_IRWXU); + auto findex = lfs->open(idx_name.back().c_str(), O_RDWR | O_CREAT | O_TRUNC, S_IRWXU); + WarpFileArgs args(findex, fmeta, ftarget); + args.virtual_size = vsize; + auto warp = create_warpfile(args, true); + ASSERT_NE(nullptr, warp); + DEFER(delete warp); + errno = 0; + EXPECT_EQ(-1, warp->can_map_staged_data()); + EXPECT_EQ(ENOSYS, errno); + map(warp, 1 << 20, 32 << 10, 16 << 10, data.get() + (16 << 10)); + verify(warp); + // unaligned mappings are rejected rather than truncated + expect_rejected(warp, 0, 100, 0, EINVAL); + expect_rejected(warp, 0, ALIGNMENT, 1, EINVAL); +} + int main(int argc, char **argv) { auto seed = 154574045; diff --git a/src/overlaybd/tar/fsmeta_file.cpp b/src/overlaybd/tar/fsmeta_file.cpp new file mode 100644 index 00000000..dc366fcd --- /dev/null +++ b/src/overlaybd/tar/fsmeta_file.cpp @@ -0,0 +1,196 @@ +/* + Copyright The Overlaybd 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 + + 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 "fsmeta_file.h" +#include +#include +#include +#include +#include + +constexpr size_t FsMetaFile::BLOCK_SIZE; + +bool FsMetaFile::dirty(size_t block) const { + return m_dirty[block / 64] & (uint64_t(1) << (block % 64)); +} + +void FsMetaFile::mark(size_t block) { + m_dirty[block / 64] |= uint64_t(1) << (block % 64); +} + +void FsMetaFile::clear(size_t block) { + m_dirty[block / 64] &= ~(uint64_t(1) << (block % 64)); +} + +bool FsMetaFile::valid_range(off_t offset, size_t count) const { + return offset >= 0 && (uint64_t)offset <= m_size && count <= m_size - offset; +} + +ssize_t FsMetaFile::pread(void *buf, size_t count, off_t offset) { + if (!valid_range(offset, count)) { + LOG_ERROR_RETURN(EINVAL, -1, "invalid fs metadata read range"); + } + size_t done = 0; + while (done < count) { + auto pos = offset + done; + auto block = pos / BLOCK_SIZE; + auto in_block = pos % BLOCK_SIZE; + auto source = dirty(block) ? m_sparse : m_base; + size_t n = std::min(count - done, BLOCK_SIZE - in_block); + if (in_block != 0 || n < BLOCK_SIZE) { + alignas(BLOCK_SIZE) char data[BLOCK_SIZE]; + auto block_bytes = std::min(BLOCK_SIZE, m_size - block * BLOCK_SIZE); + if (source->pread(data, block_bytes, block * BLOCK_SIZE) != (ssize_t)block_bytes) { + LOG_ERRNO_RETURN(EIO, -1, "failed to read fs metadata block"); + } + memcpy((char *)buf + done, data + in_block, n); + } else { + while (n + BLOCK_SIZE <= count - done && + dirty(block + n / BLOCK_SIZE) == dirty(block)) { + n += BLOCK_SIZE; + } + if (source->pread((char *)buf + done, n, pos) != (ssize_t)n) { + LOG_ERRNO_RETURN(EIO, -1, "failed to read fs metadata"); + } + } + done += n; + } + return done; +} + +ssize_t FsMetaFile::pwrite(const void *buf, size_t count, off_t offset) { + if (!valid_range(offset, count)) { + LOG_ERROR_RETURN(EINVAL, -1, "invalid fs metadata write range"); + } + size_t done = 0; + while (done < count) { + auto pos = offset + done; + auto block = pos / BLOCK_SIZE; + auto in_block = pos % BLOCK_SIZE; + size_t n = std::min(count - done, BLOCK_SIZE - in_block); + if (in_block != 0 || n < BLOCK_SIZE) { + alignas(BLOCK_SIZE) char data[BLOCK_SIZE] = {0}; + auto source = dirty(block) ? m_sparse : m_base; + auto block_bytes = std::min(BLOCK_SIZE, m_size - block * BLOCK_SIZE); + if (source->pread(data, block_bytes, block * BLOCK_SIZE) != (ssize_t)block_bytes) { + m_write_error = EIO; + LOG_ERRNO_RETURN(EIO, -1, "failed to read partial fs metadata block"); + } + memcpy(data + in_block, (const char *)buf + done, n); + if (m_sparse->pwrite(data, BLOCK_SIZE, block * BLOCK_SIZE) != (ssize_t)BLOCK_SIZE) { + m_write_error = EIO; + LOG_ERRNO_RETURN(EIO, -1, "failed to stage fs metadata block"); + } + mark(block); + } else { + n = (count - done) / BLOCK_SIZE * BLOCK_SIZE; + if (m_sparse->pwrite((const char *)buf + done, n, pos) != (ssize_t)n) { + m_write_error = EIO; + LOG_ERRNO_RETURN(EIO, -1, "failed to stage fs metadata"); + } + for (size_t i = 0; i < n / BLOCK_SIZE; i++) { + mark(block + i); + } + } + done += n; + } + return done; +} + +int FsMetaFile::fallocate(int mode, off_t offset, off_t len) { + if (len <= 0 || !valid_range(offset, len)) { + LOG_ERROR_RETURN(EINVAL, -1, "invalid fs metadata fallocate range"); + } + if (mode != (FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE) && + mode != FALLOC_FL_ZERO_RANGE && mode != (FALLOC_FL_ZERO_RANGE | FALLOC_FL_KEEP_SIZE)) { + LOG_ERROR_RETURN(ENOTSUP, -1, "unsupported fs metadata fallocate mode"); + } + if (m_base->fallocate(mode, offset, len) != 0) { + return -1; + } + auto end = offset + len; + for (auto pos = offset; pos < end;) { + auto block = pos / BLOCK_SIZE; + auto in_block = pos % BLOCK_SIZE; + size_t n = std::min(end - pos, BLOCK_SIZE - in_block); + if (dirty(block)) { + if (in_block == 0 && n == BLOCK_SIZE) { + clear(block); + } else { + alignas(BLOCK_SIZE) char data[BLOCK_SIZE]; + if (m_sparse->pread(data, BLOCK_SIZE, block * BLOCK_SIZE) != (ssize_t)BLOCK_SIZE) { + m_write_error = EIO; + LOG_ERRNO_RETURN(EIO, -1, "failed to read fs metadata before zeroing"); + } + memset(data + in_block, 0, n); + if (m_sparse->pwrite(data, BLOCK_SIZE, block * BLOCK_SIZE) != (ssize_t)BLOCK_SIZE) { + m_write_error = EIO; + LOG_ERRNO_RETURN(EIO, -1, "failed to zero staged fs metadata"); + } + } + } + pos += n; + } + return 0; +} + +void FsMetaFile::discard(off_t offset, size_t count) { + if (count == 0) { + return; + } + auto end = (offset + count + BLOCK_SIZE - 1) / BLOCK_SIZE; + for (size_t block = offset / BLOCK_SIZE; block < end; block++) { + clear(block); + } +} + +int FsMetaFile::commit() { + // Ext4's close path ignores I/O errors, including short writes to already-dirty blocks. + if (m_write_error != 0) { + LOG_ERROR_RETURN(m_write_error, -1, "fs metadata staging failed"); + } + const size_t buffer_size = 1 << 20; + void *buf = nullptr; + if (posix_memalign(&buf, BLOCK_SIZE, buffer_size) != 0) { + LOG_ERROR_RETURN(ENOMEM, -1, "failed to allocate fs metadata buffer"); + } + DEFER(free(buf)); + size_t bytes = 0; + for (size_t word = 0; word < m_dirty.size(); word++) { + while (m_dirty[word] != 0) { + size_t first = word * 64 + __builtin_ctzll(m_dirty[word]); + size_t blocks = 1; + while (blocks < buffer_size / BLOCK_SIZE && (first + blocks) * BLOCK_SIZE < m_size && + dirty(first + blocks)) { + blocks++; + } + auto offset = first * BLOCK_SIZE; + auto count = std::min(blocks * BLOCK_SIZE, m_size - offset); + if (m_sparse->pread(buf, count, offset) != (ssize_t)count) { + LOG_ERRNO_RETURN(EIO, -1, "failed to read staged fs metadata"); + } + if (m_base->pwrite(buf, count, offset) != (ssize_t)count) { + LOG_ERRNO_RETURN(EIO, -1, "failed to append fs metadata"); + } + for (size_t i = 0; i < blocks; i++) { + clear(first + i); + } + bytes += count; + } + } + LOG_INFO("appended ` bytes of compacted fs metadata", bytes); + return 0; +} diff --git a/src/overlaybd/tar/fsmeta_file.h b/src/overlaybd/tar/fsmeta_file.h new file mode 100644 index 00000000..bd81481c --- /dev/null +++ b/src/overlaybd/tar/fsmeta_file.h @@ -0,0 +1,57 @@ +/* + Copyright The Overlaybd 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 + + 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 +#include + +class FsMetaFile : public photon::fs::VirtualFile { +public: + static constexpr size_t BLOCK_SIZE = 4096; + + FsMetaFile(photon::fs::IFile *base, photon::fs::IFile *sparse, size_t size) + : m_base(base), m_sparse(sparse), m_size(size), + m_dirty((size + BLOCK_SIZE * 64 - 1) / (BLOCK_SIZE * 64)) {} + + ssize_t pread(void *buf, size_t count, off_t offset) override; + ssize_t pwrite(const void *buf, size_t count, off_t offset) override; + int fallocate(int mode, off_t offset, off_t len) override; + int fstat(struct stat *st) override { return m_base->fstat(st); } + int fsync() override { return m_sparse->fsync(); } + int fdatasync() override { return m_sparse->fdatasync(); } + int close() override { return 0; } + photon::fs::IFileSystem *filesystem() override { return m_base->filesystem(); } + + void discard(off_t offset, size_t count); + int commit(); + + UNIMPLEMENTED(int fchmod(mode_t mode) override); + UNIMPLEMENTED(int fchown(uid_t owner, gid_t group) override); + UNIMPLEMENTED(int ftruncate(off_t length) override); + +private: + photon::fs::IFile *m_base; + photon::fs::IFile *m_sparse; + size_t m_size; + std::vector m_dirty; + int m_write_error = 0; + + bool dirty(size_t block) const; + void mark(size_t block); + void clear(size_t block); + bool valid_range(off_t offset, size_t count) const; +}; diff --git a/src/overlaybd/tar/libtar.cpp b/src/overlaybd/tar/libtar.cpp index 192a4ba2..ad70be1e 100644 --- a/src/overlaybd/tar/libtar.cpp +++ b/src/overlaybd/tar/libtar.cpp @@ -23,8 +23,13 @@ #include #include #include +#include +#include +#include +#include #include #include +#include #include #include #include @@ -33,6 +38,7 @@ #include #include "../lsmt/file.h" #include "../lsmt/index.h" +#include "fsmeta_file.h" /* * Archive members may be stored with a leading / ("/usr/lib") or without @@ -139,6 +145,167 @@ ssize_t UnTar::dump_tar_headers(photon::fs::IFile *as) { return count; } +ssize_t UnTar::stage_tar(photon::fs::IFile *headers, photon::fs::IFile *target) { + const off_t base = target->lseek(0, SEEK_END); + if (base < 0) { + LOG_ERRNO_RETURN(0, -1, "failed to locate the end of the upper data file"); + } + const size_t BUF_SIZE = 1024 * 1024; + void *mem = nullptr; + if (posix_memalign(&mem, FS_BLOCKSIZE, BUF_SIZE) != 0) { + LOG_ERROR_RETURN(ENOMEM, -1, "failed to allocate the staging buffer"); + } + DEFER(free(mem)); + auto buf = (char *)mem; + size_t buffered = 0; + off_t staged = 0; + auto flush = [&]() -> int { + if (buffered == 0) { + return 0; + } + if (target->pwrite(buf, buffered, base + staged) != (ssize_t)buffered) { + LOG_ERRNO_RETURN(EIO, -1, "failed to stage ` bytes", buffered); + } + staged += buffered; + buffered = 0; + return 0; + }; + + ssize_t count = 0; + int ret; + while ((ret = read_header(headers)) == 0) { + count++; + size_t size = get_size(); + size_t left = (size + T_BLOCKSIZE - 1) & T_BLOCKMASK; + if (!TH_ISREG(header) || left == 0) { + continue; + } + auto end = headers->lseek(0, SEEK_CUR); + if (end < T_BLOCKSIZE) { + LOG_ERRNO_RETURN(0, -1, "failed to locate the dumped header of `", get_pathname()); + } + size_t pad = (size_t)(-(base + staged + buffered)) & (FS_BLOCKSIZE - 1); + memset(buf + buffered, 0, pad); + buffered += pad; + if (buffered == BUF_SIZE && flush() != 0) { + return -1; + } + off_t roffset = base + staged + buffered; + if (headers->pwrite(&roffset, sizeof(roffset), + end - T_BLOCKSIZE + offsetof(TarHeader, devmajor)) != + (ssize_t)sizeof(roffset)) { + LOG_ERRNO_RETURN(0, -1, "failed to record the offset of staged data"); + } + while (left > 0) { + auto n = std::min(left, BUF_SIZE - buffered); + if (file->read(buf + buffered, n) != (ssize_t)n) { + LOG_ERRNO_RETURN(EIO, -1, "failed to read the data of `", get_pathname()); + } + if (left == n) { + // Tar padding is arbitrary, but mapped block tails must be zero. + size_t tail = (size_t)(-size) & (T_BLOCKSIZE - 1); + memset(buf + buffered + n - tail, 0, tail); + } + buffered += n; + left -= n; + if (buffered == BUF_SIZE && flush() != 0) { + return -1; + } + } + } + if (ret != 1) { + LOG_ERRNO_RETURN(0, -1, "failed to read tar header"); + } + if (flush() != 0) { + return -1; + } + if (staged > 0) { + // the fs metadata appended next begins at a block boundary + size_t pad = (size_t)(-(base + staged)) & (FS_BLOCKSIZE - 1); + if (pad > 0) { + memset(buf, 0, pad); + if (target->pwrite(buf, pad, base + staged) != (ssize_t)pad) { + LOG_ERRNO_RETURN(EIO, -1, "failed to pad the staged data"); + } + } + } + LOG_INFO("staged ` bytes of file data at offset ` of the top layer, ` objects", staged, base, + count); + return count; +} + +int UnTar::extract_staged(FsMetaFile *fsmeta, const std::function &close_fs) { + if (fsmeta != nullptr && !close_fs) { + LOG_ERROR_RETURN(EINVAL, -1, "fs metadata staging requires closing ext4 before commit"); + } + auto lsmt = dynamic_cast(fs_base_file); + if (lsmt == nullptr || lsmt->can_map_staged_data() != 0) { + LOG_ERROR_RETURN(ENOTSUP, -1, "the base file cannot map staged data"); + } + // stage_tar() rewrote devmajor of the regular file headers + options |= TAR_IGNORE_CRC; + meta_only = from_tar_idx = defer_mapping = true; + pending.clear(); + if (extract_all() != 0) { + return -1; + } + // a freed metadata block reused for file data may still be cached dirty until now + if (fs->syncfs() != 0) { + LOG_ERRNO_RETURN(0, -1, "failed to sync fs metadata"); + } + if (close_fs) { + close_fs(); + fs = nullptr; + xattr_fs = nullptr; + } + if (fsmeta != nullptr) { + // Ext4 allocates whole blocks, so a payload extent cannot share a block with metadata. + for (auto &it : pending) { + for (auto &lba : it.second) { + fsmeta->discard(lba.offset, lba.count); + } + } + if (fsmeta->commit() != 0) { + return -1; + } + } + return apply_pending(); +} + +int UnTar::apply_pending() { + auto lsmt = static_cast(fs_base_file); + size_t mapped = 0, zeroed = 0; + for (auto &it : pending) { + for (auto &lba : it.second) { + if (lba.roffset >= 0) { + if (fs_base_file->ioctl(LSMT::IFileRW::RemoteData, lba) != (int)lba.count) { + LOG_ERRNO_RETURN(0, -1, "failed to map staged data, offset: `, count: `, roffset: `", + lba.offset, lba.count, lba.roffset); + } + mapped += lba.count; + continue; + } + // the tail of a last block reads as zero, unless the block is a reused one + LSMT::Segment s{(uint64_t)lba.offset / T_BLOCKSIZE, lba.count / T_BLOCKSIZE}; + std::vector pm(s.length); + auto n = lsmt->index()->lookup(s, pm.data(), pm.size()); + if (std::any_of(pm.begin(), pm.begin() + n, [](const LSMT::SegmentMapping &m) { + return !m.zeroed; + })) { + if (fs_base_file->fallocate(FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, lba.offset, + lba.count) != 0) { + LOG_ERRNO_RETURN(0, -1, "failed to zero stale data, offset: `, count: `", + lba.offset, lba.count); + } + zeroed += lba.count; + } + } + } + pending.clear(); + LOG_INFO("mapped ` bytes of staged data, zeroed ` bytes of stale block tails", mapped, zeroed); + return 0; +} + int UnTar::extract_all() { int i, count = 0; unpackedPaths.clear(); @@ -205,7 +372,7 @@ int UnTar::extract_file(const char *filename) { return -1; } else { if (!S_ISDIR(s.st_mode)) { - if (fs->unlink(filename) == -1 && errno != ENOENT) { + if (unlink_file(filename, s) == -1 && errno != ENOENT) { LOG_ERRNO_RETURN(EEXIST, -1, "remove exist file ` failed", filename); } } else if (!TH_ISDIR(header)) { @@ -259,6 +426,17 @@ int UnTar::extract_file(const char *filename) { return 0; } +int UnTar::unlink_file(const char *path, const struct stat &st) { + if (fs->unlink(path) != 0) { + return -1; + } + // the last link frees the blocks of the inode for reuse + if (S_ISREG(st.st_mode) && st.st_nlink <= 1) { + pending.erase(st.st_ino); + } + return 0; +} + int UnTar::extract_regfile_meta_only(const char *filename) { size_t size = get_size(); LOG_DEBUG(" ==> extracting: ` (` bytes) (turboOCIv1 index)", filename, size); @@ -270,34 +448,67 @@ int UnTar::extract_regfile_meta_only(const char *filename) { off_t p = 0; if (from_tar_idx) { - p = *((off_t*)&header.devmajor); + memcpy(&p, header.devmajor, sizeof(p)); } else { p = file->lseek(0, SEEK_CUR); } - struct photon::fs::fiemap_t<8192> fie(0, size); + // an empty file owns no block + if (size > 0 && map_regfile_data(fout, size, p) != 0) { + LOG_ERRNO_RETURN(0, -1, "failed to map the data of `", filename); + } + if (not from_tar_idx) { + file->lseek(((size+ T_BLOCKSIZE - 1) / T_BLOCKSIZE) * T_BLOCKSIZE, SEEK_CUR); // skip size + } + return 0; +} - if (fout->fallocate(0, 0, size) != 0 || fout->fiemap(&fie)!=0 ){ +// maps the blocks allocated for the file to its data at offset of the base file's backing data +int UnTar::map_regfile_data(photon::fs::IFile *fout, size_t size, off_t offset) { + if (fout->fallocate(0, 0, size) != 0) { return -1; } - auto count = ((size+ T_BLOCKSIZE - 1) / T_BLOCKSIZE) * T_BLOCKSIZE; - for (uint32_t i = 0; i < fie.fm_mapped_extents; i++) { - LSMT::RemoteMapping lba; - lba.offset = fie.fm_extents[i].fe_physical; - lba.count = (fie.fm_extents[i].fe_length < count ? fie.fm_extents[i].fe_length : count); - lba.roffset = p; - int nwrite = fs_base_file->ioctl(LSMT::IFileRW::RemoteData, lba); - if (nwrite < 0) { - LOG_ERRNO_RETURN(0, -1, "failed to write lba"); - } - p += nwrite; - count-=lba.count; + photon::fs::fiemap probe(0, size, 0); + if (fout->fiemap(&probe) != 0) { + return -1; + } + std::vector mem(sizeof(photon::fs::fiemap) + + probe.fm_mapped_extents * sizeof(photon::fs::fiemap_extent)); + auto fie = new (mem.data()) photon::fs::fiemap(0, size, probe.fm_mapped_extents); + if (fout->fiemap(fie) != 0) { + return -1; } - struct stat st; - fout->fstat(&st); - LOG_DEBUG("reg file size `", st.st_size); - if (not from_tar_idx) { - file->lseek(((size+ T_BLOCKSIZE - 1) / T_BLOCKSIZE) * T_BLOCKSIZE, SEEK_CUR); // skip size + std::vector *deferred = nullptr; + if (defer_mapping) { + struct stat st; + if (fout->fstat(&st) != 0) { + return -1; + } + deferred = &pending[st.st_ino]; + deferred->clear(); + } + size_t alignment = defer_mapping ? FS_BLOCKSIZE : T_BLOCKSIZE; + size_t data_len = (size + alignment - 1) & ~(alignment - 1); + // extents come in file order, and a file allocated in full has no hole + size_t pos = 0; + for (uint32_t i = 0; i < fie->fm_mapped_extents; i++) { + auto &e = fie->fm_extents[i]; + size_t len = pos < data_len ? std::min((size_t)e.fe_length, data_len - pos) : 0; + if (len > 0) { + LSMT::RemoteMapping lba{(off_t)e.fe_physical, (uint32_t)len, offset + (off_t)pos}; + if (deferred) { + deferred->push_back(lba); + } else if (fs_base_file->ioctl(LSMT::IFileRW::RemoteData, lba) != (int)len) { + return -1; + } + } + if (deferred && len < e.fe_length) { + deferred->push_back({(off_t)(e.fe_physical + len), (uint32_t)(e.fe_length - len), -1}); + } + pos += e.fe_length; + } + if (pos < data_len) { + LOG_ERROR_RETURN(EIO, -1, "only ` bytes are allocated for ` bytes", pos, size); } return 0; } diff --git a/src/overlaybd/tar/libtar.h b/src/overlaybd/tar/libtar.h index ad39aef3..c078b980 100644 --- a/src/overlaybd/tar/libtar.h +++ b/src/overlaybd/tar/libtar.h @@ -31,6 +31,10 @@ #include #include #include +#include +#include "../lsmt/index.h" + +class FsMetaFile; #define T_BLOCKSIZE 512 #define T_NAMELEN 100 @@ -193,7 +197,7 @@ class UnTar : public TarCore { : TarCore(src_file, options, fs_blocksize), fs(target_fs), fs_base_file(bf), meta_only(meta_only), from_tar_idx(from_tar_idx) { xattr_fs = dynamic_cast(target_fs); - if (!xattr_fs) { + if (target_fs && !xattr_fs) { LOG_WARN("xattr is not supported by target fs"); } } @@ -201,6 +205,11 @@ class UnTar : public TarCore { int extract_all(); // return number of objects in this tarfile ssize_t dump_tar_headers(photon::fs::IFile* file); + // exclusively appends payload to the underlying data file and records physical offsets + ssize_t stage_tar(photon::fs::IFile *headers, photon::fs::IFile *target); + // extracts the headers dumped by stage_tar(), mapping file blocks to the staged data + int extract_staged() { return extract_staged(nullptr, {}); } + int extract_staged(FsMetaFile *fsmeta, const std::function &close_fs); private: photon::fs::IFileSystem *fs = nullptr; // target @@ -208,12 +217,17 @@ class UnTar : public TarCore { photon::fs::IFile *fs_base_file = nullptr; bool meta_only = false; bool from_tar_idx = false; + bool defer_mapping = false; + // mappings by inode, applied once the fs metadata is synced; roffset -1 marks a tail + std::map> pending; std::set unpackedPaths; std::list> dirs; // int extract_file(const char *filename); int extract_regfile(const char *filename); int extract_regfile_meta_only(const char *filename); + int map_regfile_data(photon::fs::IFile *fout, size_t size, off_t offset); + int apply_pending(); int extract_hardlink(const char *filename); int extract_symlink(const char *filename); int extract_dir(const char *filename); @@ -221,6 +235,7 @@ class UnTar : public TarCore { int set_file_perms(const char *filename); int convert_whiteout(const char *filename); + int unlink_file(const char *path, const struct stat &st); int mkdir_hier(const std::string_view &dir); int remove_all(const std::string &path, bool rmdir = true); diff --git a/src/overlaybd/tar/test/test.cpp b/src/overlaybd/tar/test/test.cpp index 0cadd2a6..b2ead429 100644 --- a/src/overlaybd/tar/test/test.cpp +++ b/src/overlaybd/tar/test/test.cpp @@ -15,21 +15,31 @@ */ #include +#include #include #include #include #include #include #include +#include #include #include +#include +#include +#include +#include +#include +#include #include #include "../../gzindex/gzfile.h" #include "../../lsmt/file.h" #include "../libtar.h" +#include "../fsmeta_file.h" #include "../tar_file.cpp" #include "../../gzip/gz.h" #include "../../../tools/sha256file.h" +#include #define FILE_SIZE (2 * 1024 * 1024) @@ -68,10 +78,36 @@ class TarBuilder { void add_hardlink(const std::string &name, const std::string &target) { put(m_prefix + name, LNKTYPE, 0644, m_prefix + target, ""); } + // pax records for the entry that follows + void add_pax(const std::vector> &records) { + std::string content; + for (auto &r : records) { + // the length of a record counts its own digits too + auto body = " " + r.first + "=" + r.second + "\n"; + auto len = body.size() + 1; + while (std::to_string(len).size() + body.size() != len) + len = std::to_string(len).size() + body.size(); + content += std::to_string(len) + body; + } + put("././@PaxHeader", PAX_HEADER, 0644, "", content); + } + // a GNU long name for the entry that follows + void add_gnu_longname(const std::string &name) { + put("././@LongLink", GNU_LONGNAME_TYPE, 0644, "", name + '\0'); + } + // the header claims hdr_size, if given, rather than the size of the content + void add_entry(const std::string &name, char typeflag, mode_t mode, + const std::string &linkname, const std::string &content, + ssize_t hdr_size = -1) { + put(m_prefix + name, typeflag, mode, linkname, content, hdr_size); + } + size_t data_bytes() const { + return m_data_bytes; + } private: void put(const std::string &name, char typeflag, mode_t mode, - const std::string &linkname, const std::string &content) { + const std::string &linkname, const std::string &content, ssize_t hdr_size = -1) { TarHeader h; memset(&h, 0, T_BLOCKSIZE); strncpy(h.name, name.c_str(), sizeof(h.name) - 1); @@ -82,8 +118,10 @@ class TarBuilder { int_to_oct(mode, h.mode, (int)sizeof(h.mode)); int_to_oct(0, h.uid, (int)sizeof(h.uid)); int_to_oct(0, h.gid, (int)sizeof(h.gid)); - int_to_oct(content.size(), h.size, (int)sizeof(h.size)); + int_to_oct(hdr_size < 0 ? content.size() : (size_t)hdr_size, h.size, (int)sizeof(h.size)); int_to_oct(0, h.mtime, (int)sizeof(h.mtime)); + if (typeflag == REGTYPE) + m_data_bytes += (content.size() + FS_BLOCKSIZE - 1) / FS_BLOCKSIZE * FS_BLOCKSIZE; memset(h.chksum, ' ', sizeof(h.chksum)); auto raw = (unsigned char *)&h; @@ -98,7 +136,7 @@ class TarBuilder { size_t off = 0; while (off < content.size()) { auto n = std::min(content.size() - off, (size_t)T_BLOCKSIZE); - memset(block, 0, T_BLOCKSIZE); + memset(block, typeflag == REGTYPE ? 0x5a : 0, T_BLOCKSIZE); memcpy(block, content.data() + off, n); m_file->write(block, T_BLOCKSIZE); off += n; @@ -108,6 +146,7 @@ class TarBuilder { photon::fs::IFile *m_file = nullptr; std::string m_prefix; + size_t m_data_bytes = 0; }; class TarTest : public ::testing::Test { @@ -636,6 +675,827 @@ TEST_F(TarTest, layer_names_rooted) { check_layer_names("/"); } +namespace { + +uint64_t pad_to(uint64_t x, uint64_t alignment) { + return (x + alignment - 1) / alignment * alignment; +} + +// reads the way a pipe does: neither seeking nor positional reads +class PipeLikeFile : public photon::fs::ForwardFile_Ownership { +public: + explicit PipeLikeFile(photon::fs::IFile *file) : photon::fs::ForwardFile_Ownership(file, true) { + } + off_t lseek(off_t, int) override { + errno = ESPIPE; + return -1; + } + ssize_t pread(void *, size_t, off_t) override { + errno = ESPIPE; + return -1; + } +}; + +std::string payload(size_t size, uint32_t seed) { + std::string s(size, '\0'); + uint32_t x = seed * 2654435761u + 1; + for (auto &c : s) { + x = x * 1103515245 + 12345; + c = x >> 16; + } + return s; +} + +std::string slurp(photon::fs::IFileSystem *fs, const std::string &path) { + std::unique_ptr f(fs->open(path.c_str(), O_RDONLY)); + if (!f) { + ADD_FAILURE() << "failed to open " << path; + return {}; + } + std::string content; + std::vector buf(64 << 10); + ssize_t n; + while ((n = f->read(buf.data(), buf.size())) > 0) + content.append(buf.data(), n); + EXPECT_EQ(0, n) << path; + return content; +} + +uint64_t fnv1a(const std::string &s) { + uint64_t h = 1469598103934665603ULL; + for (unsigned char c : s) { + h ^= c; + h *= 1099511628211ULL; + } + return h; +} + +int gzip_to(photon::fs::IFileSystem *fs, const std::string &src, const std::string &dst) { + auto content = slurp(fs, src); + gzFile gz = gzopen(dst.c_str(), "wb"); + if (gz == nullptr) + return -1; + auto n = gzwrite(gz, content.data(), content.size()); + return (gzclose(gz) == Z_OK && n == (int)content.size()) ? 0 : -1; +} + +std::string child_path(const std::string &dir, const std::string &name) { + return (dir == "/" ? std::string() : dir) + "/" + name; +} + +std::vector dir_entries(photon::fs::IFileSystem *fs, const std::string &dir) { + std::vector names; + auto d = fs->opendir(dir.c_str()); + if (d == nullptr) { + ADD_FAILURE() << "failed to open dir " << dir; + return names; + } + while (auto de = d->readdir()) { + std::string name = de->d_name; + if (name != "." && name != "..") + names.push_back(name); + } + fs->closedir(d); + std::sort(names.begin(), names.end()); + return names; +} + +// path -> what is observable about the entry, the content of a regular file included +using Manifest = std::map; + +void describe_tree(photon::fs::IFileSystem *fs, const std::string &dir, Manifest &manifest, + std::map &links) { + auto xfs = dynamic_cast(fs); + for (auto &name : dir_entries(fs, dir)) { + auto path = child_path(dir, name); + struct stat st; + if (fs->lstat(path.c_str(), &st) != 0) { + ADD_FAILURE() << "failed to lstat " << path; + continue; + } + std::ostringstream os; + os << std::oct << st.st_mode << std::dec << " " << st.st_uid << ":" << st.st_gid + << " nlink=" << st.st_nlink; + // a directory changed by later entries keeps the time of the change + if (!S_ISDIR(st.st_mode)) { + os << " mtime=" << st.st_mtime; + if (st.st_nlink > 1) + os << " link=" << links.emplace(st.st_ino, path).first->second; + } + if (S_ISREG(st.st_mode)) { + os << " size=" << st.st_size << " fnv=" << std::hex << fnv1a(slurp(fs, path)) + << std::dec; + } else if (S_ISLNK(st.st_mode)) { + char target[4096]; + auto n = fs->readlink(path.c_str(), target, sizeof(target)); + // the extfs counts a terminating NUL in + os << " target=" << std::string(target, strnlen(target, n > 0 ? n : 0)); + } + if (xfs) { + char list[4096]; + auto n = xfs->llistxattr(path.c_str(), list, sizeof(list)); + std::vector xnames; + for (ssize_t i = 0; i < n; i += strlen(list + i) + 1) + xnames.emplace_back(list + i); + std::sort(xnames.begin(), xnames.end()); + for (auto &x : xnames) { + char value[4096]; + auto m = xfs->lgetxattr(path.c_str(), x.c_str(), value, sizeof(value)); + os << " " << x << "=" << std::string(value, m > 0 ? m : 0); + } + } + manifest[path] = os.str(); + if (S_ISDIR(st.st_mode)) + describe_tree(fs, path, manifest, links); + } +} + +Manifest describe_tree(photon::fs::IFileSystem *fs) { + Manifest manifest; + std::map links; + describe_tree(fs, "/", manifest, links); + return manifest; +} + +void compare_trees(const Manifest &expected, const Manifest &actual) { + for (auto &e : expected) { + auto it = actual.find(e.first); + if (it == actual.end()) + ADD_FAILURE() << "missing " << e.first; + else + EXPECT_EQ(e.second, it->second) << e.first; + } + for (auto &a : actual) { + if (!expected.count(a.first)) + ADD_FAILURE() << "unexpected " << a.first; + } +} + +struct FileBlocks { + size_t size = 0; + std::vector> data, tails; // +}; +using Files = std::map; + +void file_blocks(photon::fs::IFileSystem *fs, const std::string &dir, Files &files) { + for (auto &name : dir_entries(fs, dir)) { + auto path = child_path(dir, name); + struct stat st; + ASSERT_EQ(0, fs->lstat(path.c_str(), &st)) << path; + if (S_ISDIR(st.st_mode)) { + file_blocks(fs, path, files); + ASSERT_FALSE(::testing::Test::HasFatalFailure()); + continue; + } + if (!S_ISREG(st.st_mode) || st.st_size == 0 || files.count(st.st_ino)) + continue; + std::unique_ptr f(fs->open(path.c_str(), O_RDONLY)); + ASSERT_NE(nullptr, f) << path; + std::unique_ptr> fie( + new photon::fs::fiemap_t<1024>(0, st.st_size)); + ASSERT_EQ(0, f->fiemap(fie.get())) << path; + ASSERT_LT(fie->fm_mapped_extents, 1024u) << path; + auto &fb = files[st.st_ino]; + fb.size = st.st_size; + // the extents come in file order, without fe_logical + uint64_t data_len = pad_to(st.st_size, FS_BLOCKSIZE), pos = 0; + uint64_t tail_begin = pad_to(st.st_size, T_BLOCKSIZE); + for (uint32_t i = 0; i < fie->fm_mapped_extents; i++) { + auto &e = fie->fm_extents[i]; + uint64_t len = pos < data_len ? std::min(e.fe_length, data_len - pos) : 0; + if (len > 0) + fb.data.emplace_back(e.fe_physical, len); + uint64_t tail = pos < tail_begin ? std::min(e.fe_length, tail_begin - pos) : 0; + if (tail < e.fe_length) + fb.tails.emplace_back(e.fe_physical + tail, e.fe_length - tail); + pos += e.fe_length; + } + EXPECT_GE(pos, data_len) << path; + } +} + +Files file_blocks(photon::fs::IFileSystem *fs) { + Files files; + file_blocks(fs, "/", files); + return files; +} + +// Checks the sealed top layer of `layers`: the file data staged at [4096, payload_end), +// the fs metadata after it up to data_end, then the index and the trailer. Of `files`, +// those whose data is mapped by the top layer must be single runs of the staged data; +// there must be `staged_files` of them. +void check_staged_layout(std::vector layers, off_t payload_end, + off_t data_end, const Files &files, size_t staged_files) { + auto top = layers.back(); + std::unique_ptr ro(LSMT::open_file_ro(top, false)); + ASSERT_NE(nullptr, ro); + auto index = ro->index(); + auto n = index->size(); + ASSERT_GT(n, 0UL); + struct stat st; + ASSERT_EQ(0, top->fstat(&st)); + auto bytes = n * sizeof(LSMT::SegmentMapping); + EXPECT_EQ(data_end + (off_t)pad_to(bytes, T_BLOCKSIZE) + 4096, st.st_size); + // as stored, i.e. without a tag + std::vector stored(n); + ASSERT_EQ((ssize_t)bytes, top->pread(stored.data(), bytes, data_end)); + EXPECT_EQ(0, memcmp(stored.data(), index->buffer(), bytes)); + + uint64_t end = 0, payload_bytes = 0, meta_bytes = 0; + for (size_t i = 0; i < n; i++) { + auto &m = index->buffer()[i]; + EXPECT_LE(end, (uint64_t)m.offset); + end = m.end(); + if (m.zeroed) + continue; + uint64_t mbegin = m.moffset * T_BLOCKSIZE, mend = m.mend() * T_BLOCKSIZE; + EXPECT_LE(4096UL, mbegin); + EXPECT_LE(mend, (uint64_t)data_end); + if (mend <= (uint64_t)payload_end) + payload_bytes += m.length * T_BLOCKSIZE; + else if (mbegin >= (uint64_t)payload_end) + meta_bytes += m.length * T_BLOCKSIZE; + else + ADD_FAILURE() << "a mapping straddles the end of the file data: [" << mbegin << ", " + << mend << ")"; + } + EXPECT_GT(payload_bytes, 0UL); + EXPECT_GT(meta_bytes, 0UL); + + std::vector> runs; + std::vector pm(8192); + for (auto &it : files) { + auto &fb = it.second; + uint64_t data_len = pad_to(fb.size, FS_BLOCKSIZE), pos = 0, mapped = 0; + int64_t base = INT64_MIN; + bool single_run = true; + for (auto &piece : fb.data) { + for (uint64_t off = 0; off < piece.second; off += 4 << 20) { + auto len = std::min(4 << 20, piece.second - off); + LSMT::Segment s{(piece.first + off) / T_BLOCKSIZE, (uint32_t)(len / T_BLOCKSIZE)}; + auto cnt = index->lookup(s, pm.data(), pm.size()); + for (size_t j = 0; j < cnt; j++) { + auto &x = pm[j]; + uint64_t file_pos = pos + x.offset * T_BLOCKSIZE - piece.first; + int64_t b = x.zeroed ? -1 : (int64_t)(x.moffset * T_BLOCKSIZE) - (int64_t)file_pos; + if (base == INT64_MIN) + base = b; + single_run &= (b == base); + mapped += x.length * T_BLOCKSIZE; + } + } + pos += piece.second; + } + if (mapped == 0) + continue; // staged in a lower layer + EXPECT_EQ(data_len, mapped) << "inode " << it.first; + EXPECT_TRUE(single_run) << "inode " << it.first; + EXPECT_LE(4096L, base) << "inode " << it.first; + EXPECT_EQ(0L, base % FS_BLOCKSIZE) << "inode " << it.first; + EXPECT_LE(base + (int64_t)data_len, (int64_t)payload_end) << "inode " << it.first; + if (fb.size % FS_BLOCKSIZE != 0) { + std::vector last(FS_BLOCKSIZE); + ASSERT_EQ(FS_BLOCKSIZE, top->pread(last.data(), last.size(), base + data_len - FS_BLOCKSIZE)); + EXPECT_TRUE(std::all_of(last.begin() + fb.size % FS_BLOCKSIZE, last.end(), + [](char c) { return c == 0; })) << "inode " << it.first; + } + runs.emplace_back(base, base + data_len); + } + EXPECT_EQ(staged_files, runs.size()); + std::sort(runs.begin(), runs.end()); + for (size_t i = 1; i < runs.size(); i++) + EXPECT_LE(runs[i - 1].second, runs[i].first); + + // the slack of a last block reads as zero, even if the block held something else before + std::unique_ptr image(LSMT::open_files_ro(layers.data(), layers.size(), false)); + ASSERT_NE(nullptr, image); + std::vector buf, zero; + for (auto &it : files) { + for (auto &t : it.second.tails) { + buf.resize(t.second); + zero.assign(t.second, 0); + ASSERT_EQ((ssize_t)t.second, image->pread(buf.data(), t.second, t.first)); + EXPECT_EQ(0, memcmp(buf.data(), zero.data(), t.second)) + << "stale data in the last block of inode " << it.first << " at " << t.first; + } + } +} + +} // namespace + +class StagedTarTest : public TarTest, public ::testing::WithParamInterface {}; + +TEST_P(StagedTarTest, staged_extract) { + for (auto fn : {"staged_1.tar", "staged_2.tar", "staged_2.tar.gz", "staged_1.spool", + "staged_2.spool", "staged_1.data", "staged_1.idx", "staged_2.data", + "staged_2.idx", "staged_scratch.data", "staged_scratch.idx", "staged_ref.img", + "staged_meta.sparse"}) + filelist.push_back(fn); + + uint32_t seed = 0; + using Contents = std::map; + auto add = [&](TarBuilder &tb, Contents &files, const std::string &name, size_t size) { + auto content = payload(size, ++seed); + tb.add_file(name, content); + files["/" + name] = content; + }; + + Contents files1, files2; + size_t data_bytes1, data_bytes2; + const std::string pax_path = "pax/" + std::string(150, 'x'); + const std::string gnu_path = "gnu/" + std::string(140, 'g'); + { + TarBuilder tb(fs, "staged_1.tar", ""); + ASSERT_TRUE(tb.ok()); + tb.add_dir("data"); + for (size_t size : {0, 1, 511, 512, 513, 4095, 4096, 4097}) + add(tb, files1, "data/s" + std::to_string(size), size); + tb.add_dir("gone"); + add(tb, files1, "gone/x", 4096); + // emptied again, they leave holes for layer 2 to fill + tb.add_dir("frag"); + for (int i = 0; i < 64; i++) + add(tb, files1, "frag/h" + std::to_string(i), 4096); + for (int i = 1; i < 64; i += 2) + add(tb, files1, "frag/h" + std::to_string(i), 0); + // more than a mapping can hold + add(tb, files1, "big", (9 << 20) + 123); + tb.add_dir("pax"); + auto content = payload(5000, ++seed); + // the size of the pax record rules + tb.add_pax({{"path", pax_path}, {"SCHILY.xattr.user.origin", "pax"}, {"size", "5000"}}); + tb.add_entry(pax_path, REGTYPE, 0640, "", content, 0); + files1["/" + pax_path] = content; + tb.add_dir("gnu"); + content = payload(70000, ++seed); + tb.add_gnu_longname(gnu_path); + tb.add_entry(gnu_path, REGTYPE, 0644, "", content); + files1["/" + gnu_path] = content; + tb.add_hardlink("data/link-to-4097", "data/s4097"); + files1["/data/link-to-4097"] = files1["/data/s4097"]; + tb.add_symlink("data/sym", "s4096"); + add(tb, files1, "data/dup", 10000); + add(tb, files1, "data/dup", 3000); + // replaced, but still linked + add(tb, files1, "data/orig", 6000); + tb.add_hardlink("data/keep", "data/orig"); + files1["/data/keep"] = files1["/data/orig"]; + add(tb, files1, "data/orig", 100); + tb.add_dir("opq"); + add(tb, files1, "opq/lower", 2000); + data_bytes1 = tb.data_bytes(); + } + { + files2 = files1; + for (auto it = files2.begin(); it != files2.end();) { + if (it->first.rfind("/frag/", 0) == 0 || it->first.rfind("/gone/", 0) == 0 || + it->first == "/data/s4095" || it->first == "/opq/lower") + it = files2.erase(it); + else + ++it; + } + TarBuilder tb(fs, "staged_2.tar", ""); + ASSERT_TRUE(tb.ok()); + // a mounted fs allocates blocks from its start, each time going on from where it + // left off: the first files get the blocks of s4095 and of the replaced s4096, + // which hold more data in layer 1 than the new files do + tb.add_dir("data"); + tb.add_file("data/.wh.s4095", ""); + add(tb, files2, "data/s4096", 100); + tb.add_dir("opq"); + tb.add_file("opq/.wh..wh..opq", ""); + add(tb, files2, "opq/after-opq", 2048); + // big2 gets the block of dir gone, changed and freed but maybe not written back + // yet, then the holes layer 1 left between frag/h*, and more + tb.add_file(".wh.gone", ""); + add(tb, files2, "big2", 300000); + tb.add_file(".wh.frag", ""); + tb.add_hardlink("data/l2-link", "data/s1"); + files2["/data/l2-link"] = files2["/data/s1"]; + data_bytes2 = tb.data_bytes(); + } + EXPECT_EQ(311296UL, data_bytes2); + ASSERT_EQ(0, gzip_to(fs, "staged_2.tar", workdir + "/staged_2.tar.gz")); + + auto open_new = [&](const char *fn) { + std::unique_ptr f(fs->open(fn, O_RDWR | O_CREAT | O_TRUNC, 0644)); + EXPECT_NE(nullptr, f) << fn; + return f; + }; + auto file_size = [](photon::fs::IFile *f) { + struct stat st; + EXPECT_EQ(0, f->fstat(&st)); + return st.st_size; + }; + auto create_layer = [](photon::fs::IFile *fdata, photon::fs::IFile *findex) { + LSMT::LayerInfo args(fdata, findex); + args.virtual_size = IMAGE_SIZE; + return std::unique_ptr(LSMT::create_file_rw(args, false)); + }; + auto replay = [&](photon::fs::IFile *headers, LSMT::IFileRW *img, Files &blocks, bool mkfs) { + ASSERT_EQ(0, headers->lseek(0, SEEK_SET)); + auto sparse = open_new("staged_meta.sparse"); + ASSERT_NE(nullptr, sparse); + FsMetaFile fsmeta(img, sparse.get(), IMAGE_SIZE); + auto backend = GetParam() ? (photon::fs::IFile *)&fsmeta : img; + if (mkfs) { + ASSERT_EQ(0, make_extfs(backend)); + } + std::unique_ptr vfs( + new_extfs_with_buffer(backend, GetParam() ? 0 : 64 << 10, 4 << 10)); + ASSERT_NE(nullptr, vfs); + UnTar tar(headers, vfs.get(), 0, 4096, img); + if (GetParam()) { + auto data_end = file_size(img->get_lower_files().back()); + ASSERT_EQ(0, tar.extract_staged(&fsmeta, [&]() { + EXPECT_EQ(data_end, file_size(img->get_lower_files().back())); + blocks = file_blocks(vfs.get()); + vfs.reset(); + })); + } else { + ASSERT_EQ(0, tar.extract_staged()); + vfs.reset(new_extfs(img, false)); + ASSERT_NE(nullptr, vfs); + blocks = file_blocks(vfs.get()); + } + }; + auto check_compacted = [&](photon::fs::IFile *data, off_t payload_end, off_t data_end) { + if (!GetParam()) { + return; + } + std::unique_ptr ro(LSMT::open_file_ro(data, false)); + ASSERT_NE(nullptr, ro); + std::vector meta; + auto index = ro->index(); + for (size_t i = 0; i < index->size(); i++) { + auto m = index->buffer()[i]; + if (!m.zeroed && m.moffset * T_BLOCKSIZE >= (uint64_t)payload_end) { + meta.push_back(m); + } + } + std::sort(meta.begin(), meta.end(), [](const auto &a, const auto &b) { + return a.moffset < b.moffset; + }); + uint64_t physical_end = payload_end, logical_end = 0; + for (auto &m : meta) { + EXPECT_EQ(physical_end, m.moffset * T_BLOCKSIZE); + EXPECT_LE(logical_end, (uint64_t)m.offset); + physical_end = m.mend() * T_BLOCKSIZE; + logical_end = m.end(); + } + EXPECT_EQ((uint64_t)data_end, physical_end); + }; + // as `overlaybd-commit --seal` does + auto seal = [](photon::fs::IFile *fdata, photon::fs::IFile *findex) { + std::unique_ptr rw(LSMT::open_file_rw(fdata, findex, false)); + ASSERT_NE(nullptr, rw); + ASSERT_EQ(0, rw->close_seal()); + }; + + // layer 1, from a seekable tar + auto d1 = open_new("staged_1.data"), i1 = open_new("staged_1.idx"); + auto spool1 = open_new("staged_1.spool"); + ASSERT_TRUE(d1 && i1 && spool1); + off_t payload_end1, data_end1; + Files blocks1; + { + auto rw = create_layer(d1.get(), i1.get()); + ASSERT_NE(nullptr, rw); + std::unique_ptr src(fs->open("staged_1.tar", O_RDONLY)); + ASSERT_NE(nullptr, src); + UnTar scan(src.get(), nullptr, 0, 4096); + EXPECT_LT(0, scan.stage_tar(spool1.get(), rw->get_lower_files().back())); + payload_end1 = file_size(d1.get()); + EXPECT_EQ((off_t)(4096 + pad_to(data_bytes1, 4096)), payload_end1); + // nothing is mapped yet + EXPECT_EQ(0UL, rw->index()->size()); + replay(spool1.get(), rw.get(), blocks1, true); + ASSERT_FALSE(HasFatalFailure()); + } + data_end1 = file_size(d1.get()); + seal(d1.get(), i1.get()); + ASSERT_FALSE(HasFatalFailure()); + auto sealed1 = file_size(d1.get()); + check_staged_layout({d1.get()}, payload_end1, data_end1, blocks1, blocks1.size()); + check_compacted(d1.get(), payload_end1, data_end1); + ASSERT_FALSE(HasFatalFailure()); + + // layer 2, from a gzipped stream on top of layer 1 + auto d2 = open_new("staged_2.data"), i2 = open_new("staged_2.idx"); + auto spool2 = open_new("staged_2.spool"); + ASSERT_TRUE(d2 && i2 && spool2); + off_t payload_end2, data_end2; + Files blocks2; + { + auto rw = create_layer(d2.get(), i2.get()); + ASSERT_NE(nullptr, rw); + std::unique_ptr lower(LSMT::open_file_ro(d1.get(), false)); + ASSERT_NE(nullptr, lower); + std::unique_ptr img(LSMT::stack_files(rw.get(), lower.get(), false, false)); + ASSERT_NE(nullptr, img); + auto gz = open_gzfile_adaptor((workdir + "/staged_2.tar.gz").c_str()); + ASSERT_NE(nullptr, gz); + PipeLikeFile src(gz); + UnTar scan(&src, nullptr, 0, 4096); + EXPECT_LT(0, scan.stage_tar(spool2.get(), img->get_lower_files().back())); + payload_end2 = file_size(d2.get()); + EXPECT_EQ((off_t)(4096 + pad_to(data_bytes2, 4096)), payload_end2); + replay(spool2.get(), img.get(), blocks2, false); + ASSERT_FALSE(HasFatalFailure()); + } + EXPECT_EQ(sealed1, file_size(d1.get())); + data_end2 = file_size(d2.get()); + seal(d2.get(), i2.get()); + ASSERT_FALSE(HasFatalFailure()); + check_staged_layout({d1.get(), d2.get()}, payload_end2, data_end2, blocks2, 3); + check_compacted(d2.get(), payload_end2, data_end2); + ASSERT_FALSE(HasFatalFailure()); + + // what extracting the layers directly gives + Manifest ref1, ref2; + { + auto raw = open_new("staged_ref.img"); + ASSERT_NE(nullptr, raw); + ASSERT_EQ(0, raw->ftruncate(IMAGE_SIZE)); + ASSERT_EQ(0, make_extfs(raw.get())); + std::unique_ptr ref(new_extfs(raw.get(), false)); + ASSERT_NE(nullptr, ref); + auto extract = [&](const char *fn, Manifest &manifest) { + std::unique_ptr tarf(fs->open(fn, O_RDONLY)); + ASSERT_NE(nullptr, tarf); + UnTar tar(tarf.get(), ref.get(), 0, 4096); + ASSERT_EQ(0, tar.extract_all()); + manifest = describe_tree(ref.get()); + }; + extract("staged_1.tar", ref1); + ASSERT_FALSE(HasFatalFailure()); + extract("staged_2.tar", ref2); + ASSERT_FALSE(HasFatalFailure()); + } + + auto view = [&](std::vector layers, const Manifest &expected, + const Contents &files, const std::function &check) { + std::unique_ptr ro(LSMT::open_files_ro(layers.data(), layers.size(), false)); + ASSERT_NE(nullptr, ro); + // the fs writes its superblock when unmounted + auto sd = open_new("staged_scratch.data"), si = open_new("staged_scratch.idx"); + ASSERT_TRUE(sd && si); + auto scratch = create_layer(sd.get(), si.get()); + ASSERT_NE(nullptr, scratch); + std::unique_ptr img(LSMT::stack_files(scratch.get(), ro.get(), false, false)); + ASSERT_NE(nullptr, img); + std::unique_ptr vfs(new_extfs(img.get(), false)); + ASSERT_NE(nullptr, vfs); + compare_trees(expected, describe_tree(vfs.get())); + for (auto &f : files) + EXPECT_TRUE(slurp(vfs.get(), f.first) == f.second) << f.first; + check(vfs.get()); + }; + + view({d1.get()}, ref1, files1, [&](photon::fs::IFileSystem *vfs) { + struct stat st; + // an extent of big is longer than a mapping can be + ASSERT_EQ(0, vfs->lstat("/big", &st)); + ASSERT_EQ(1UL, blocks1.count(st.st_ino)); + uint64_t longest = 0; + for (auto &piece : blocks1[st.st_ino].data) + longest = std::max(longest, piece.second); + EXPECT_LT((uint64_t)LSMT::Segment::MAX_LENGTH * T_BLOCKSIZE, longest); + auto pax = "/" + pax_path; + ASSERT_EQ(0, vfs->lstat(pax.c_str(), &st)); + EXPECT_EQ(0640U, st.st_mode & 07777); + EXPECT_EQ(5000, st.st_size); + auto xfs = dynamic_cast(vfs); + ASSERT_NE(nullptr, xfs); + char value[16] = {0}; + EXPECT_EQ(3, xfs->lgetxattr(pax.c_str(), "user.origin", value, sizeof(value))); + EXPECT_STREQ("pax", value); + }); + ASSERT_FALSE(HasFatalFailure()); + + view({d1.get(), d2.get()}, ref2, files2, [&](photon::fs::IFileSystem *vfs) { + struct stat st, st2; + for (auto gone : {"/frag", "/gone", "/data/s4095", "/opq/lower"}) + EXPECT_EQ(-1, vfs->lstat(gone, &st)) << gone; + std::unique_ptr lower(LSMT::open_file_ro(d1.get(), false)); + ASSERT_NE(nullptr, lower); + // with more extents than an inode holds, the first one on fs metadata of layer 1 + ASSERT_EQ(0, vfs->lstat("/big2", &st)); + ASSERT_EQ(1UL, blocks2.count(st.st_ino)); + auto &big2 = blocks2[st.st_ino].data; + EXPECT_LT(4UL, big2.size()); + LSMT::SegmentMapping m; + ASSERT_EQ(1UL, lower->index()->lookup(LSMT::Segment{big2[0].first / T_BLOCKSIZE, 1}, &m, 1)); + EXPECT_LE((uint64_t)payload_end1, m.moffset * T_BLOCKSIZE); + // the slack of their last blocks, checked to read as zero, has data in layer 1 + for (auto reused : {"/data/s4096", "/opq/after-opq"}) { + ASSERT_EQ(0, vfs->lstat(reused, &st)); + ASSERT_EQ(1UL, blocks2.count(st.st_ino)); + auto &tails = blocks2[st.st_ino].tails; + ASSERT_EQ(1UL, tails.size()) << reused; + std::string stale(tails[0].second, '\0'); + ASSERT_EQ((ssize_t)stale.size(), lower->pread(&stale[0], stale.size(), tails[0].first)); + EXPECT_NE(std::string::npos, stale.find_first_not_of('\0')) << reused; + } + ASSERT_EQ(0, vfs->lstat("/data/link-to-4097", &st)); + ASSERT_EQ(0, vfs->lstat("/data/s4097", &st2)); + EXPECT_EQ(st2.st_ino, st.st_ino); + ASSERT_EQ(0, vfs->lstat("/data/l2-link", &st)); + ASSERT_EQ(0, vfs->lstat("/data/s1", &st2)); + EXPECT_EQ(st2.st_ino, st.st_ino); + char target[64] = {0}; + ASSERT_LT(0, vfs->readlink("/data/sym", target, sizeof(target))); + EXPECT_STREQ("s4096", target); + ASSERT_EQ(0, vfs->lstat("/data/keep", &st)); + EXPECT_EQ(6000, st.st_size); + ASSERT_EQ(0, vfs->lstat("/data/orig", &st)); + EXPECT_EQ(100, st.st_size); + }); +} + +INSTANTIATE_TEST_SUITE_P(FsMeta, StagedTarTest, ::testing::Bool()); + +class FsMetaTest : public TarTest { +protected: + class TrackingFile : public photon::fs::ForwardFile { + public: + explicit TrackingFile(photon::fs::IFile *file) : ForwardFile(file) {} + std::vector> writes; + off_t fail_offset = -1; + off_t short_offset = -1; + + ssize_t pwrite(const void *buf, size_t count, off_t offset) override { + if (offset == fail_offset) { + errno = EIO; + return -1; + } + if (offset == short_offset) { + return m_file->pwrite(buf, count / 2, offset); + } + auto ret = m_file->pwrite(buf, count, offset); + if (ret == (ssize_t)count) { + writes.emplace_back(offset, count); + } + return ret; + } + }; + + void SetUp() override { + TarTest::SetUp(); + filelist.push_back("fsmeta_base.img"); + filelist.push_back("fsmeta_staging.img"); + base.reset(fs->open("fsmeta_base.img", O_RDWR | O_CREAT | O_TRUNC, 0644)); + sparse.reset(fs->open("fsmeta_staging.img", O_RDWR | O_CREAT | O_TRUNC, 0644)); + ASSERT_TRUE(base && sparse); + ASSERT_EQ(0, base->ftruncate(IMAGE_SIZE)); + tracked.reset(new TrackingFile(base.get())); + overlay.reset(new FsMetaFile(tracked.get(), sparse.get(), IMAGE_SIZE)); + } + + std::unique_ptr base, sparse; + std::unique_ptr tracked; + std::unique_ptr overlay; +}; + +TEST_F(FsMetaTest, merged_reads_and_partial_writes) { + std::string initial(5 * 4096, 'a'); + ASSERT_EQ((ssize_t)initial.size(), base->pwrite(initial.data(), initial.size(), 0)); + std::string expected = initial; + std::string whole(4096, 'b'), partial(5000, 'c'); + ASSERT_EQ((ssize_t)whole.size(), overlay->pwrite(whole.data(), whole.size(), 2 * 4096)); + expected.replace(2 * 4096, whole.size(), whole); + ASSERT_EQ((ssize_t)partial.size(), overlay->pwrite(partial.data(), partial.size(), 4096 - 123)); + expected.replace(4096 - 123, partial.size(), partial); + std::string got(initial.size(), '\0'); + ASSERT_EQ((ssize_t)got.size(), overlay->pread(&got[0], got.size(), 0)); + EXPECT_EQ(expected, got); + ASSERT_EQ(73, overlay->pread(&got[0], 73, 8192 + 17)); + EXPECT_EQ(expected.substr(8192 + 17, 73), got.substr(0, 73)); + ASSERT_EQ((ssize_t)initial.size(), base->pread(&got[0], initial.size(), 0)); + EXPECT_EQ(initial, got); + EXPECT_TRUE(tracked->writes.empty()); + ASSERT_EQ(0, overlay->commit()); + ASSERT_EQ((ssize_t)got.size(), base->pread(&got[0], got.size(), 0)); + EXPECT_EQ(expected, got); + ASSERT_EQ(1UL, tracked->writes.size()); + EXPECT_EQ(0, tracked->writes[0].first); + EXPECT_EQ(3 * 4096UL, tracked->writes[0].second); + ASSERT_EQ(0, overlay->commit()); + EXPECT_EQ(1UL, tracked->writes.size()); + whole.assign(4096, 'd'); + ASSERT_EQ(4096, overlay->pwrite(whole.data(), whole.size(), 4096)); + ASSERT_EQ(0, overlay->commit()); + EXPECT_EQ(2UL, tracked->writes.size()); +} + +TEST_F(FsMetaTest, zero_and_discard) { + std::string expected(4 * 4096, 'a'); + ASSERT_EQ((ssize_t)expected.size(), base->pwrite(expected.data(), expected.size(), 0)); + std::string dirty(3 * 4096, 'b'); + ASSERT_EQ((ssize_t)dirty.size(), overlay->pwrite(dirty.data(), dirty.size(), 0)); + expected.replace(0, dirty.size(), dirty); + ASSERT_EQ(0, overlay->fallocate(FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, 512, 512)); + expected.replace(512, 512, std::string(512, '\0')); + ASSERT_EQ(0, overlay->fallocate(FALLOC_FL_ZERO_RANGE, 4096, 4096)); + expected.replace(4096, 4096, std::string(4096, '\0')); + overlay->discard(8192 + 512, 512); + expected.replace(8192, 4096, std::string(4096, 'a')); + std::string got(expected.size(), '\0'); + ASSERT_EQ((ssize_t)got.size(), overlay->pread(&got[0], got.size(), 0)); + EXPECT_EQ(expected, got); + ASSERT_EQ(0, overlay->commit()); + ASSERT_EQ(1UL, tracked->writes.size()); + EXPECT_EQ(0, tracked->writes[0].first); + EXPECT_EQ(4096UL, tracked->writes[0].second); + ASSERT_EQ((ssize_t)got.size(), base->pread(&got[0], got.size(), 0)); + EXPECT_EQ(expected, got); +} + +TEST_F(FsMetaTest, sparse_sorted_commit_and_retry) { + std::string block(4096, 'x'); + for (size_t n : {100000, 64, 0, 63, 62}) { + ASSERT_EQ(4096, overlay->pwrite(block.data(), block.size(), n * 4096)); + } + for (int i = 0; i < 10; i++) { + ASSERT_EQ(4096, overlay->pwrite(block.data(), block.size(), 64 * 4096)); + } + struct stat st; + ASSERT_EQ(0, sparse->fstat(&st)); + EXPECT_LT((uint64_t)st.st_blocks * 512, (uint64_t)st.st_size / 100); + EXPECT_TRUE(tracked->writes.empty()); + tracked->fail_offset = 62 * 4096; + ASSERT_EQ(-1, overlay->commit()); + ASSERT_EQ(1UL, tracked->writes.size()); + tracked->fail_offset = -1; + ASSERT_EQ(0, overlay->commit()); + ASSERT_EQ(3UL, tracked->writes.size()); + EXPECT_EQ(0, tracked->writes[0].first); + EXPECT_EQ(62 * 4096, tracked->writes[1].first); + EXPECT_EQ(3 * 4096UL, tracked->writes[1].second); + EXPECT_EQ(100000 * 4096, tracked->writes[2].first); + std::string got(4096, '\0'); + ASSERT_EQ(4096, overlay->pread(&got[0], got.size(), 100000 * 4096)); + EXPECT_EQ(block, got); +} + +TEST_F(FsMetaTest, bounds_and_large_run) { + char byte = 0; + EXPECT_EQ(-1, overlay->pread(&byte, 1, -1)); + EXPECT_EQ(-1, overlay->pwrite(&byte, 1, IMAGE_SIZE)); + EXPECT_EQ(-1, overlay->fallocate(FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, IMAGE_SIZE, 4096)); + EXPECT_EQ(-1, overlay->fallocate(0, 0, 4096)); + EXPECT_EQ(0, overlay->pwrite(&byte, 0, IMAGE_SIZE)); + EXPECT_EQ(0, overlay->pread(&byte, 0, IMAGE_SIZE)); + std::string data((1 << 20) + 4096, 'z'); + ASSERT_EQ((ssize_t)data.size(), overlay->pwrite(data.data(), data.size(), 4096)); + ASSERT_EQ(0, overlay->commit()); + ASSERT_EQ(2UL, tracked->writes.size()); + EXPECT_EQ(4096, tracked->writes[0].first); + EXPECT_EQ(1UL << 20, tracked->writes[0].second); + EXPECT_EQ((1 << 20) + 4096, tracked->writes[1].first); + EXPECT_EQ(4096UL, tracked->writes[1].second); +} + +TEST_F(FsMetaTest, rejects_failed_staging) { + for (bool short_write : {false, true}) { + TrackingFile staging(sparse.get()); + FsMetaFile fsmeta(tracked.get(), &staging, IMAGE_SIZE); + std::string block(4096, 'a'); + ASSERT_EQ(4096, fsmeta.pwrite(block.data(), block.size(), 0)); + if (short_write) { + staging.short_offset = 0; + } else { + staging.fail_offset = 0; + } + block.assign(4096, 'b'); + ASSERT_EQ(-1, fsmeta.pwrite(block.data(), block.size(), 0)); + staging.fail_offset = staging.short_offset = -1; + ASSERT_EQ(-1, fsmeta.commit()); + EXPECT_TRUE(tracked->writes.empty()); + } +} + +TEST_F(FsMetaTest, final_partial_block) { + size_t size = 64 * 4096 + 512; + ASSERT_EQ(0, base->ftruncate(size)); + FsMetaFile fsmeta(tracked.get(), sparse.get(), size); + std::string data(512, 'p'), got(512, '\0'); + ASSERT_EQ(512, base->pwrite(data.data(), data.size(), size - 512)); + ASSERT_EQ(512, fsmeta.pread(&got[0], got.size(), size - 512)); + EXPECT_EQ(data, got); + char byte = 'q'; + ASSERT_EQ(1, fsmeta.pwrite(&byte, 1, size - 1)); + ASSERT_EQ(512, fsmeta.pread(&got[0], got.size(), size - 512)); + data.back() = byte; + EXPECT_EQ(data, got); + ASSERT_EQ(0, fsmeta.commit()); + ASSERT_EQ(512, base->pread(&got[0], got.size(), size - 512)); + EXPECT_EQ(data, got); + struct stat st; + ASSERT_EQ(0, base->fstat(&st)); + EXPECT_EQ((off_t)size, st.st_size); +} + TEST(CleanNameTest, clean_name) { char name[256] = {0}; char *cname; diff --git a/src/overlaybd/tar/whiteout.cpp b/src/overlaybd/tar/whiteout.cpp index 462e5473..c7f21f97 100644 --- a/src/overlaybd/tar/whiteout.cpp +++ b/src/overlaybd/tar/whiteout.cpp @@ -54,7 +54,7 @@ int UnTar::remove_all(const std::string &path, bool rmdir) { if (fs->lstat(path.c_str(), &statBuf) == 0) { // get stat if (S_ISDIR(statBuf.st_mode) == 0) { // not dir if (unpackedPaths.find(path) == unpackedPaths.end()) { - fs->unlink(path.c_str()); + unlink_file(path.c_str(), statBuf); } return 0; } diff --git a/src/tools/overlaybd-apply.cpp b/src/tools/overlaybd-apply.cpp index 0ac1a8e8..8f24d3cd 100644 --- a/src/tools/overlaybd-apply.cpp +++ b/src/tools/overlaybd-apply.cpp @@ -40,6 +40,7 @@ #include "comm_func.h" #include "sha256file.h" #include "qcow2converter.h" +#include "../overlaybd/tar/fsmeta_file.h" using namespace std; @@ -79,6 +80,22 @@ class FIFOFile : public VirtualReadOnlyFile { UNIMPLEMENTED(ssize_t preadv(const struct iovec *iov, int iovcnt, off_t offset) override); }; +// the file is gone as soon as it is closed +static photon::fs::IFile *create_temp_file() { + auto dir = getenv("TMPDIR"); + std::string path = std::string(dir && *dir ? dir : "/tmp") + "/overlaybd-apply-XXXXXX"; + int fd = mkstemp(&path[0]); + if (fd < 0) { + LOG_ERRNO_RETURN(0, nullptr, "failed to create temporary file `", path); + } + unlink(path.c_str()); + auto file = new_localfile_adaptor(fd); + if (file == nullptr) { + close(fd); + } + return file; +} + int main(int argc, char **argv) { std::string image_config_path, input_path, gz_index_path, config_path, sha256_checksum; string tarheader; @@ -133,8 +150,18 @@ int main(int argc, char **argv) { } bool gen_turboOCI = (gz_index_path != "" ); + photon::fs::IFile* base_file = raw ? nullptr : ((ImageFile *)imgfile)->get_base(); + // a plain append-mode layer gets all the file data ahead of the fs metadata + auto staging = gen_turboOCI ? nullptr : dynamic_cast(base_file); + if (staging && staging->can_map_staged_data() != 0) { + staging = nullptr; + } - auto target = create_ext4fs(imgfile, mkfs, !gen_turboOCI); + std::unique_ptr fsmeta_sparse; + std::unique_ptr fsmeta; + // with staging, the fs (mkfs included) is created once the file data is in place + photon::fs::IFileSystem *target = + staging ? nullptr : create_ext4fs(imgfile, mkfs, !gen_turboOCI); DEFER({ delete target; }); photon::fs::IFile* src_file = nullptr; @@ -167,14 +194,7 @@ int main(int argc, char **argv) { src_file = checksum_file = new_sha256_file(src_file, true); } - photon::fs::IFile* base_file = raw ? nullptr : ((ImageFile *)imgfile)->get_base(); - - auto tar = new UnTar(src_file, target, 0, 4096, base_file, gen_turboOCI); - - if (tar->extract_all() < 0) { - fprintf(stderr, "failed to extract\n"); - exit(-1); - } else { + auto verify_checksum = [&]() { if (checksum_file != nullptr) { auto calc = checksum_file->sha256_checksum(); if (calc != sha256_checksum) { @@ -182,9 +202,51 @@ int main(int argc, char **argv) { exit(-1); } } - fprintf(stdout, "overlaybd-apply done\n"); - fprintf(stderr, "%s\n", sha256_checksum.c_str()); + }; + + if (staging) { + auto headers = create_temp_file(); + if (headers == nullptr) { + fprintf(stderr, "failed to create a temporary file\n"); + exit(-1); + } + DEFER(delete headers); + UnTar scan(src_file, nullptr, 0, 4096); + if (scan.stage_tar(headers, staging->get_lower_files().back()) < 0) { + fprintf(stderr, "failed to stage file data\n"); + exit(-1); + } + verify_checksum(); + if (headers->lseek(0, SEEK_SET) != 0) { + fprintf(stderr, "failed to rewind the tar headers\n"); + exit(-1); + } + fsmeta_sparse.reset(create_temp_file()); + struct stat image_st; + if (fsmeta_sparse == nullptr || imgfile->fstat(&image_st) != 0) { + fprintf(stderr, "failed to create fs metadata staging file\n"); + return -1; + } + fsmeta.reset(new FsMetaFile(imgfile, fsmeta_sparse.get(), image_st.st_size)); + target = create_ext4fs(fsmeta.get(), mkfs, false); + UnTar replay(headers, target, 0, 4096, base_file); + if (replay.extract_staged(fsmeta.get(), [&]() { + delete target; + target = nullptr; + }) < 0) { + fprintf(stderr, "failed to extract\n"); + return -1; + } + } else { + auto tar = new UnTar(src_file, target, 0, 4096, base_file, gen_turboOCI); + if (tar->extract_all() < 0) { + fprintf(stderr, "failed to extract\n"); + exit(-1); + } + verify_checksum(); } + fprintf(stdout, "overlaybd-apply done\n"); + fprintf(stderr, "%s\n", sha256_checksum.c_str()); return 0; }