Skip to content

Commit 2a4e130

Browse files
committed
Remove path member from zzip so it can be tested in memory only, and write tests.
1 parent 6daf8cf commit 2a4e130

File tree

8 files changed

+353
-107
lines changed

8 files changed

+353
-107
lines changed

src/debug_menu.cpp

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -483,8 +483,7 @@ void write_min_archive()
483483
{
484484
std::optional<zzip> min_mmr_temp_zzip = zzip::load( min_mmr_temp_zzip_path, mmr_dict );
485485
if( !min_mmr_temp_zzip ||
486-
!mmr_stack->copy_files_to( trimmed_mmr_entries, *min_mmr_temp_zzip ) ||
487-
!min_mmr_temp_zzip->compact( 0 ) ) {
486+
!mmr_stack->copy_files_to( trimmed_mmr_entries, *min_mmr_temp_zzip ) ) {
488487
popup( _( "Failed to create minimized archive" ) );
489488
return;
490489
}

src/game.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3497,7 +3497,8 @@ bool game::save_player_data()
34973497
".zzip.tmp" ).get_unrelative_path() );
34983498
saved_data = z->add_file( ( playerfile + SAVE_EXTENSION ).get_unrelative_path().filename(),
34993499
save.str() );
3500-
saved_data = saved_data && z->compact( 1.0 );
3500+
saved_data = saved_data && z->compact_to( ( playerfile + SAVE_EXTENSION +
3501+
".zzip" ).get_unrelative_path(), 0.0 );
35013502
} else {
35023503
saved_data = write_to_file( playerfile + SAVE_EXTENSION, [&]( std::ostream & fout ) {
35033504
serialize_json( fout );

src/mapbuffer.cpp

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,12 +253,12 @@ void mapbuffer::save_quad(
253253
bool file_exists = false;
254254

255255
std::optional<zzip> z;
256+
cata_path zzip_name = dirname;
257+
zzip_name += ".zzip";
256258
// The number of uniform submaps is so enormous that the filesystem overhead
257259
// for this step of just checking if the quad exists approaches 70% of the
258260
// total cost of saving the mapbuffer, in one test save I had.
259261
if( world_generator->active_world->has_compression_enabled() ) {
260-
cata_path zzip_name = dirname;
261-
zzip_name += ".zzip";
262262
z = zzip::load( zzip_name.get_unrelative_path(),
263263
( PATH_INFO::world_base_save_path() / "maps.dict" ).get_unrelative_path() );
264264
if( !z ) {
@@ -341,7 +341,6 @@ void mapbuffer::save_quad(
341341

342342
if( z ) {
343343
z->add_file( filename.get_relative_path().filename(), s );
344-
z->compact( 2.0 );
345344
} else {
346345
// Don't create the directory if it would be empty
347346
assure_dir_exist( dirname );
@@ -357,6 +356,13 @@ void mapbuffer::save_quad(
357356
std::filesystem::remove( filename.get_unrelative_path() );
358357
}
359358
}
359+
if( z ) {
360+
cata_path tmp_path = zzip_name + ".tmp";
361+
if( z->compact_to( tmp_path.get_unrelative_path(), 2.0 ) ) {
362+
z.reset();
363+
rename_file( tmp_path, zzip_name );
364+
}
365+
}
360366
}
361367

362368
// We're reading in way too many entities here to mess around with creating sub-objects and

src/overmap.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7251,7 +7251,11 @@ void overmap::save() const
72517251
throw std::runtime_error( string_format( "Failed to save omap %d.%d to %s", loc.x(),
72527252
loc.y(), zzip_path.get_unrelative_path().generic_u8string().c_str() ) );
72537253
}
7254-
z->compact( 2.0 );
7254+
cata_path tmp_path = zzip_path + ".tmp";
7255+
if( z->compact_to( tmp_path.get_unrelative_path(), 2.0 ) ) {
7256+
z.reset();
7257+
rename_file( tmp_path, zzip_path );
7258+
}
72557259
} else {
72567260
write_to_file( PATH_INFO::world_base_save_path() / overmapbuffer::terrain_filename( loc ), [&](
72577261
std::ostream & stream ) {

src/zzip.cpp

Lines changed: 50 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -339,9 +339,8 @@ struct zzip::context {
339339
ZSTD_DCtx *dctx;
340340
};
341341

342-
zzip::zzip( std::filesystem::path path, std::shared_ptr<mmap_file> file, JsonObject footer )
343-
: path_{ std::move( path ) },
344-
file_{ std::move( file ) },
342+
zzip::zzip( std::shared_ptr<mmap_file> file, JsonObject footer )
343+
: file_{ std::move( file ) },
345344
footer_{ std::move( footer ) }
346345
{}
347346

@@ -358,6 +357,12 @@ std::optional<zzip> zzip::load( std::filesystem::path const &path,
358357
{
359358
std::shared_ptr<mmap_file> file = mmap_file::map_writeable_file( path );
360359

360+
return load( std::move( file ), dictionary_path );
361+
}
362+
std::optional<zzip> zzip::load(
363+
std::shared_ptr<mmap_file> file,
364+
std::filesystem::path const &dictionary_path )
365+
{
361366
flexbuffers::Reference root;
362367
std::shared_ptr<parsed_flexbuffer> flexbuffer;
363368
JsonObject footer;
@@ -386,7 +391,7 @@ std::optional<zzip> zzip::load( std::filesystem::path const &path,
386391
}
387392
}
388393

389-
std::optional<zzip> ret{ std::in_place, zzip{path, std::move( file ), std::move( footer )} };
394+
std::optional<zzip> ret{ std::in_place, zzip{std::move( file ), std::move( footer )} };
390395
zzip &zip = ret.value();
391396

392397
ZSTD_CCtx *cctx;
@@ -471,7 +476,7 @@ bool zzip::add_file( std::filesystem::path const &zzip_relative_path, std::strin
471476

472477

473478
bool zzip::copy_files( std::vector<std::filesystem::path> const &zzip_relative_paths,
474-
zzip const &from )
479+
zzip const &from, bool shrink_to_fit )
475480
{
476481
if( zzip_relative_paths.empty() ) {
477482
return true;
@@ -505,7 +510,7 @@ bool zzip::copy_files( std::vector<std::filesystem::path> const &zzip_relative_p
505510
entry_to_copy.offset = new_content_end;
506511
new_content_end += entry_to_copy.len;
507512
}
508-
return update_footer( original_footer, new_content_end, entries_to_copy );
513+
return update_footer( original_footer, new_content_end, entries_to_copy, shrink_to_fit );
509514
}
510515

511516

@@ -602,11 +607,6 @@ size_t zzip::get_zzip_size() const
602607
return file_->len();
603608
}
604609

605-
std::filesystem::path const &zzip::get_path() const
606-
{
607-
return path_;
608-
}
609-
610610
std::vector<std::filesystem::path> zzip::get_entries() const
611611
{
612612
zzip_footer footer{ footer_ };
@@ -650,14 +650,23 @@ JsonObject zzip::copy_footer() const
650650
}
651651
}
652652

653+
namespace
654+
{
655+
size_t round_up_file_size( size_t bytes )
656+
{
657+
// Round up to a whole page. Drives nowadays are all 4k pages.
658+
size_t needed_pages = std::max( size_t{ 1 }, ( bytes + kAssumedPageSize - 1 ) / kAssumedPageSize );
659+
660+
return needed_pages * kAssumedPageSize;
661+
}
662+
}
663+
653664
// Ensures the zzip file has room to write at least this many bytes.
654665
// Returns the guaranteed size of the file, which is at least bytes large.
655666
size_t zzip::ensure_capacity_for( size_t bytes )
656667
{
657668
// Round up to a whole page. Drives nowadays are all 4k pages.
658-
size_t needed_pages = std::max( size_t{1}, ( bytes + kAssumedPageSize - 1 ) / kAssumedPageSize );
659-
660-
size_t new_size = needed_pages * kAssumedPageSize;
669+
size_t new_size = round_up_file_size( bytes );
661670

662671
if( file_->len() < new_size && !file_->resize_file( new_size ) ) {
663672
return 0;
@@ -730,7 +739,8 @@ size_t zzip::write_file_at( std::string_view filename, std::string_view content,
730739
// original JsonObject and inserting the given new entries.
731740
// If shrink_to_fit is true, will shrink the file as needed to eliminate padding bytes
732741
// between the content and the footer.
733-
bool zzip::update_footer( JsonObject const &original_footer, size_t content_end,
742+
bool zzip::update_footer( JsonObject const &original_footer,
743+
size_t content_end,
734744
const std::vector<compressed_entry> &entries, bool shrink_to_fit )
735745
{
736746
std::unordered_set<std::string> processed_files;
@@ -779,12 +789,12 @@ bool zzip::update_footer( JsonObject const &original_footer, size_t content_end,
779789
auto buf = builder.GetBuffer();
780790

781791
if( shrink_to_fit ) {
782-
return false;
783792
if( !file_->resize_file( content_end + buf.size() ) ) {
793+
return false;
784794
}
785795
} else {
786-
size_t guaranteed_capacity = ensure_capacity_for( content_end + buf.size() );
787-
if( guaranteed_capacity == 0 ) {
796+
size_t guaranteed_capacity = round_up_file_size( content_end + buf.size() );
797+
if( guaranteed_capacity == 0 || !file_->resize_file( guaranteed_capacity ) ) {
788798
return false;
789799
}
790800
}
@@ -803,13 +813,13 @@ bool zzip::update_footer( JsonObject const &original_footer, size_t content_end,
803813
std::shared_ptr<parsed_flexbuffer> new_flexbuffer = std::make_shared<zzip_flexbuffer>(
804814
std::move( new_storage )
805815
);
806-
footer_ = JsonValue( std::move( new_flexbuffer ), new_root, nullptr, 0 );
807816

817+
footer_ = JsonValue( std::move( new_flexbuffer ), new_root, nullptr, 0 );
808818
return true;
809819
}
810820

811821
// Scans the zzip to read what data we can validate and write a fresh footer.
812-
bool zzip::rewrite_footer()
822+
bool zzip::rewrite_footer( bool shrink_to_fit )
813823
{
814824
const size_t zzip_len = file_->len();
815825
size_t scan_offset = 0;
@@ -910,7 +920,7 @@ bool zzip::rewrite_footer()
910920
entries.emplace_back( std::move( entry ) );
911921
}
912922

913-
return update_footer( JsonObject{}, scan_offset, entries, /* shrink_to_fit = */ true );
923+
return update_footer( JsonObject{}, scan_offset, entries, shrink_to_fit );
914924
}
915925

916926
std::optional<zzip> zzip::create_from_folder( std::filesystem::path const &path,
@@ -1031,12 +1041,17 @@ bool zzip::extract_to_folder( std::filesystem::path const &path,
10311041
return false;
10321042
}
10331043

1034-
for( const JsonMember &entry : zip->footer_.get_object( kEntriesKey ) ) {
1044+
return zip->extract_to_folder( folder );
1045+
}
1046+
1047+
bool zzip::extract_to_folder( std::filesystem::path const &folder )
1048+
{
1049+
for( const JsonMember &entry : footer_.get_object( kEntriesKey ) ) {
10351050
if( entry.name().empty() ) {
10361051
continue;
10371052
}
10381053
std::filesystem::path filename = std::filesystem::u8path( entry.name() );
1039-
size_t len = zip->get_file_size( filename );
1054+
size_t len = get_file_size( filename );
10401055
std::filesystem::path destination_file_path = folder / filename;
10411056
if( !assure_dir_exist( destination_file_path.parent_path() ) ) {
10421057
return false;
@@ -1045,7 +1060,7 @@ bool zzip::extract_to_folder( std::filesystem::path const &path,
10451060
if( !file || !file->resize_file( len ) ) {
10461061
return false;
10471062
}
1048-
if( zip->get_file_to( filename, static_cast<std::byte *>( file->base() ), file->len() ) != len ) {
1063+
if( get_file_to( filename, static_cast<std::byte *>( file->base() ), file->len() ) != len ) {
10491064
return false;
10501065
}
10511066
}
@@ -1111,7 +1126,7 @@ bool zzip::delete_files( std::unordered_set<std::filesystem::path, std_fs_path_h
11111126
return !errored;
11121127
}
11131128

1114-
bool zzip::compact( double bloat_factor )
1129+
bool zzip::compact_to( std::filesystem::path dest, double bloat_factor )
11151130
{
11161131
zzip_footer footer{ footer_ };
11171132
std::optional<zzip_meta> meta_opt = footer.get_meta();
@@ -1135,69 +1150,26 @@ bool zzip::compact( double bloat_factor )
11351150
}
11361151
}
11371152

1138-
std::vector<compressed_entry> compressed_entries = footer.get_entries();
1139-
1140-
// If we were compacting in place, sorting left to right would ensure that we
1141-
// only move entries left that we need to move. Unchanged data would stay on the left.
1142-
// However that exposes us to data loss on power loss because zzip recovery
1143-
// relies on an intact sequence of zstd frames.
1144-
std::sort( compressed_entries.begin(), compressed_entries.end(), []( const compressed_entry & lhs,
1145-
const compressed_entry & rhs ) {
1146-
return lhs.offset < rhs.offset;
1147-
} );
1148-
1149-
std::filesystem::path tmp_path = path_;
1150-
tmp_path.replace_extension( ".zzip.tmp" ); // NOLINT(cata-u8-path)
1151-
1152-
std::shared_ptr<mmap_file> compacted_file = mmap_file::map_writeable_file( tmp_path );
1153-
if( !compacted_file ||
1154-
!compacted_file->resize_file( meta.total_content_size + kFixedSizeOverhead ) ) {
1153+
std::unique_ptr<mmap_file> compacted_file = mmap_file::map_writeable_file( dest );
1154+
if( !compacted_file ) {
11551155
return false;
11561156
}
11571157

1158-
size_t current_end = kFooterChecksumFrameSize;
1159-
for( compressed_entry &entry : compressed_entries ) {
1160-
memcpy( static_cast<char *>( compacted_file->base() ) + current_end,
1161-
file_base_plus( entry.offset ),
1162-
entry.len
1163-
);
1164-
entry.offset = current_end;
1165-
current_end += entry.len;
1166-
}
1167-
1168-
// We can swap the old mmap'd file with the new one
1169-
// and write a fresh footer directly into it.
1170-
JsonObject old_footer{ copy_footer() };
1171-
old_footer.allow_omitted_members();
1172-
1173-
file_ = compacted_file;
1174-
compacted_file = nullptr;
1175-
1176-
std::error_code ec;
1177-
on_out_of_scope reset_on_failure{ [&] {
1178-
file_ = mmap_file::map_writeable_file( path_ );
1179-
std::filesystem::remove( tmp_path, ec );
1180-
} };
1181-
1182-
if( !update_footer( old_footer, current_end, compressed_entries, /* shrink_to_fit = */ true ) ) {
1183-
return false;
1184-
}
1158+
return compact_to( std::move( compacted_file ) );
1159+
}
11851160

1186-
// Swap the tmp file over the real path. Give it a couple tries in case of filesystem races.
1187-
size_t attempts = 0;
1188-
do {
1189-
std::filesystem::rename( tmp_path, path_, ec );
1190-
} while( ec && ++attempts < 3 );
1191-
if( ec ) {
1161+
bool zzip::compact_to( std::shared_ptr<mmap_file> dest )
1162+
{
1163+
std::optional<zzip> new_zip = zzip::load( dest );
1164+
if( !new_zip ) {
11921165
return false;
11931166
}
1194-
reset_on_failure.cancel();
1195-
return true;
1167+
return new_zip->copy_files( get_entries(), *this, /* shrink_to_fit = */ true );
11961168
}
11971169

11981170
bool zzip::clear()
11991171
{
1200-
return file_->resize_file( 0 ) && rewrite_footer();
1172+
return file_->resize_file( 0 ) && rewrite_footer( /* shrink_to_fit = */ true );
12011173
}
12021174

12031175
// Can't directly increment void*, have to cast to char* first.

0 commit comments

Comments
 (0)