// SZArchive.mm — Main archive interface implementation

#include "SZBridgeCommon.h"
#include "SZCallbacks.h"

#include <memory>
#include <string>
#include <vector>

#import "../Utilities/SZObjCLog.h"

#include <objc/objc-sync.h>

#ifdef __APPLE__
#include <sys/xattr.h>
#ifndef XATTR_NOFOLLOW
#define XATTR_NOFOLLOW 0x0001
#endif
#endif

#import "../Utilities/SZOperationSessionDefaults.h"

#include "7zVersion.h"
#include "CPP/7zip/Common/MethodProps.h"
#include "CPP/7zip/UI/Agent/Agent.h"
#include "CPP/7zip/UI/Common/ArchiveExtractCallback.h"
#include "CPP/7zip/UI/Common/EnumDirItems.h"
#include "CPP/7zip/UI/Common/Extract.h"
#include "CPP/7zip/UI/Common/ExtractingFilePath.h"
#include "CPP/7zip/UI/Common/HashCalc.h"
#include "CPP/7zip/UI/Common/OpenArchive.h"
#include "CPP/7zip/UI/Common/PropIDUtils.h"
#include "CPP/7zip/UI/Common/SetProperties.h"
#include "CPP/7zip/UI/Common/Update.h"
#include "CPP/7zip/UI/Common/UpdateCallback.h"
#include "CPP/7zip/UI/Common/WorkDir.h"
#include "CPP/Common/MyString.h"
#include "CPP/Common/StringToInt.h"
#include "CPP/Common/Wildcard.h"
#include "CPP/Windows/ErrorMsg.h"
#include "CPP/Windows/System.h"

// ============================================================
// ObjC model implementations
// ============================================================

@implementation SZCompressionSettings
- (instancetype)init {
    if ((self = [super init])) {
        _format = SZArchiveFormat7z;
        _level = SZCompressionLevelNormal;
        _levelValue = SZCompressionLevelNormal;
        _method = SZCompressionMethodLZMA2;
        _encryption = SZEncryptionMethodNone;
        _updateMode = SZCompressionUpdateModeAdd;
        _pathMode = SZCompressionPathModeRelativePaths;
        _methodName = @"LZMA2";
        _solidMode = YES;
        _openSharedFiles = NO;
        _deleteAfterCompression = NO;
        _storeSymbolicLinks = SZCompressionBoolSettingNotDefined;
        _storeHardLinks = SZCompressionBoolSettingNotDefined;
        _storeAlternateDataStreams = SZCompressionBoolSettingNotDefined;
        _storeFileSecurity = SZCompressionBoolSettingNotDefined;
        _preserveSourceAccessTime = SZCompressionBoolSettingNotDefined;
        _storeModificationTime = SZCompressionBoolSettingNotDefined;
        _storeCreationTime = SZCompressionBoolSettingNotDefined;
        _storeAccessTime = SZCompressionBoolSettingNotDefined;
        _setArchiveTimeToLatestFile = SZCompressionBoolSettingNotDefined;
        _timePrecision = SZCompressionTimePrecisionAutomatic;
    }
    return self;
}
@end

@implementation SZExtractionSettings
- (instancetype)init {
    if ((self = [super init])) {
        _pathMode = SZPathModeFullPaths;
        _overwriteMode = SZOverwriteModeAsk;
        _preserveNtSecurityInfo = NO;
    }
    return self;
}
@end

static NSData* SZQuarantineDataForArchivePath(NSString* archivePath) {
    if (!archivePath || archivePath.length == 0) {
        return nil;
    }

    const char* fsPath = archivePath.fileSystemRepresentation;
    if (!fsPath) {
        return nil;
    }

    static NSString* const quarantineAttributeName = @"com.apple.quarantine";
    const char* attrName = quarantineAttributeName.fileSystemRepresentation;
    if (!attrName) {
        return nil;
    }

    // Avoid a size-probe/read race by trying a fixed buffer first.
    char stackBuffer[1024];
    ssize_t read = getxattr(fsPath, attrName, stackBuffer, sizeof(stackBuffer), 0, XATTR_NOFOLLOW);
    if (read >= 0) {
        return [NSData dataWithBytes:stackBuffer length:(NSUInteger)read];
    }
    if (errno != ERANGE) {
        return nil;
    }

    // Rare fallback for larger xattrs.
    for (int attempt = 0; attempt < 4; attempt++) {
        const ssize_t probed = getxattr(fsPath, attrName, NULL, 0, 0, XATTR_NOFOLLOW);
        if (probed < 0) {
            return nil;
        }
        NSMutableData* data = [NSMutableData dataWithLength:(NSUInteger)probed];
        read = getxattr(fsPath, attrName, data.mutableBytes, data.length, 0, XATTR_NOFOLLOW);
        if (read >= 0) {
            data.length = (NSUInteger)read;
            return data;
        }
        if (errno != ERANGE) {
            return nil;
        }
    }
    return nil;
}

@implementation SZArchiveEntry
- (instancetype)init {
    if ((self = [super init])) {
        _path = @"";
        _pathParts = @[];
        _propertyValues = @{ };
    }
    return self;
}
@end
@implementation SZArchiveEntryProperty
@end
@implementation SZFormatInfo
@end
@implementation SZCompressionResourceInfo
@end
@implementation SZBenchDisplayRow
- (instancetype)init {
    if ((self = [super init])) {
        _sizeText = @"...";
        _speedText = @"...";
        _usageText = @"...";
        _rpuText = @"...";
        _ratingText = @"...";
    }
    return self;
}
@end

@implementation SZBenchSnapshot
- (instancetype)init {
    if ((self = [super init])) {
        _passesCompleted = 0;
        _passesTotal = 1;
        _finished = NO;
        _logText = @"";
    }
    return self;
}
@end

namespace {

class SZArchiveOperationGuard {
public:
    explicit SZArchiveOperationGuard(id object)
        : _object(object) { objc_sync_enter(_object); }
    ~SZArchiveOperationGuard() { objc_sync_exit(_object); }

private:
    __unsafe_unretained id _object;
};

enum SZCompressionEstimateMethodID {
    kSZCompressionEstimateCopy,
    kSZCompressionEstimateLZMA,
    kSZCompressionEstimateLZMA2,
    kSZCompressionEstimatePPMd,
    kSZCompressionEstimateBZip2,
    kSZCompressionEstimateDeflate,
    kSZCompressionEstimateDeflate64,
    kSZCompressionEstimatePPMdZip,
#if SHICHIZIP_ZS_VARIANT
    kSZCompressionEstimateFastLzma2,
    kSZCompressionEstimateZstd,
    kSZCompressionEstimateBrotli,
    kSZCompressionEstimateLz4,
    kSZCompressionEstimateLz5,
    kSZCompressionEstimateLizardFastLz4,
    kSZCompressionEstimateLizardLizV1,
    kSZCompressionEstimateLizardFastLz4Huffman,
    kSZCompressionEstimateLizardLizV1Huffman,
#endif
    kSZCompressionEstimateGnu,
    kSZCompressionEstimatePosix,
};

static const UInt32 kSZCompressionEstimateLzmaMaxDictSize = (UInt32)15 << 28;
#if SHICHIZIP_ZS_VARIANT
static const NSInteger kSZCompressionZstdFastLevelIncrement = 32;
static const NSInteger kSZCompressionZstdUltimateLevel = 255;
#endif

struct SZCompressionEstimateRamInfo {
    bool IsDefined;
    UInt64 RamSizeReduced;
    UInt64 UsageAuto;
};

static NSString* SZArchiveCodecNameForCreateFormat(SZArchiveFormat format) {
    switch (format) {
    case SZArchiveFormat7z:
        return @"7z";
    case SZArchiveFormatZip:
        return @"zip";
    case SZArchiveFormatTar:
        return @"tar";
    case SZArchiveFormatGZip:
        return @"gzip";
    case SZArchiveFormatBZip2:
        return @"bzip2";
    case SZArchiveFormatXz:
        return @"xz";
    case SZArchiveFormatWim:
        return @"wim";
#if SHICHIZIP_ZS_VARIANT
    case SZArchiveFormatZstd:
        return @"zstd";
    case SZArchiveFormatBrotli:
        return @"brotli";
    case SZArchiveFormatLizard:
        return @"lizard";
    case SZArchiveFormatLz4:
        return @"lz4";
    case SZArchiveFormatLz5:
        return @"lz5";
#endif
    default:
        return nil;
    }
}

static bool SZCompressionEstimateMethodSupportsSFX(int methodID) {
    switch (methodID) {
    case kSZCompressionEstimateCopy:
    case kSZCompressionEstimateLZMA:
    case kSZCompressionEstimateLZMA2:
    case kSZCompressionEstimatePPMd:
#if SHICHIZIP_ZS_VARIANT
    case kSZCompressionEstimateFastLzma2:
    case kSZCompressionEstimateZstd:
#endif
        return true;
    default:
        return false;
    }
}

static bool SZCompressionEstimateFormatSupportsFilters(SZArchiveFormat format) {
    return format == SZArchiveFormat7z;
}

static bool SZCompressionEstimateFormatSupportsThreads(SZArchiveFormat format) {
    switch (format) {
    case SZArchiveFormat7z:
    case SZArchiveFormatZip:
    case SZArchiveFormatBZip2:
    case SZArchiveFormatXz:
#if SHICHIZIP_ZS_VARIANT
    case SZArchiveFormatZstd:
    case SZArchiveFormatBrotli:
    case SZArchiveFormatLizard:
    case SZArchiveFormatLz4:
    case SZArchiveFormatLz5:
#endif
        return true;
    default:
        return false;
    }
}

static bool
SZCompressionEstimateFormatSupportsMemoryUse(SZArchiveFormat format) {
    switch (format) {
    case SZArchiveFormat7z:
    case SZArchiveFormatZip:
    case SZArchiveFormatGZip:
    case SZArchiveFormatBZip2:
    case SZArchiveFormatXz:
#if SHICHIZIP_ZS_VARIANT
    case SZArchiveFormatZstd:
    case SZArchiveFormatBrotli:
    case SZArchiveFormatLizard:
    case SZArchiveFormatLz4:
    case SZArchiveFormatLz5:
#endif
        return true;
    default:
        return false;
    }
}

static bool SZCompressionEstimateIsZipFormat(SZArchiveFormat format) {
    return format == SZArchiveFormatZip;
}

static bool SZCompressionEstimateIsXzFormat(SZArchiveFormat format) {
    return format == SZArchiveFormatXz;
}

static int SZCompressionEstimateLevel(SZCompressionSettings* settings) {
    return (int)settings.levelValue;
}

#if SHICHIZIP_ZS_VARIANT
static bool SZCompressionEstimateIsZstdFastLevel(int level) {
    return level < 0;
}

static UInt32 SZCompressionEstimateZstdFastLevel(int level) {
    return level < 0 ? (UInt32)(-level) : 0;
}
#endif

static UInt32 SZCompressionLevelPropertyValue(int methodID,
    NSInteger levelValue) {
#if SHICHIZIP_ZS_VARIANT
    if (methodID == kSZCompressionEstimateZstd && levelValue < 0) {
        return (UInt32)(kSZCompressionZstdFastLevelIncrement - levelValue);
    }
#endif
    return (UInt32)levelValue;
}

static int SZCompressionEstimateMethodID(SZCompressionSettings* settings) {
    NSString* methodName = settings.methodName ? settings.methodName.lowercaseString : @"";
    if (methodName.length > 0) {
        if ([methodName isEqualToString:@"copy"]) {
            return kSZCompressionEstimateCopy;
        }
        if ([methodName isEqualToString:@"lzma"]) {
            return kSZCompressionEstimateLZMA;
        }
        if ([methodName isEqualToString:@"lzma2"]) {
            return kSZCompressionEstimateLZMA2;
        }
        if ([methodName isEqualToString:@"ppmd"]) {
            return settings.format == SZArchiveFormatZip
                ? kSZCompressionEstimatePPMdZip
                : kSZCompressionEstimatePPMd;
        }
        if ([methodName isEqualToString:@"bzip2"]) {
            return kSZCompressionEstimateBZip2;
        }
        if ([methodName isEqualToString:@"deflate"]) {
            return kSZCompressionEstimateDeflate;
        }
        if ([methodName isEqualToString:@"deflate64"]) {
            return kSZCompressionEstimateDeflate64;
        }
#if SHICHIZIP_ZS_VARIANT
        if ([methodName isEqualToString:@"flzma2"]) {
            return kSZCompressionEstimateFastLzma2;
        }
        if ([methodName isEqualToString:@"zstd"]) {
            return kSZCompressionEstimateZstd;
        }
        if ([methodName isEqualToString:@"brotli"]) {
            return kSZCompressionEstimateBrotli;
        }
        if ([methodName isEqualToString:@"lz4"]) {
            return kSZCompressionEstimateLz4;
        }
        if ([methodName isEqualToString:@"lz5"]) {
            return kSZCompressionEstimateLz5;
        }
        if ([methodName isEqualToString:@"lizard-fastlz4"]) {
            return kSZCompressionEstimateLizardFastLz4;
        }
        if ([methodName isEqualToString:@"lizard-lizv1"]) {
            return kSZCompressionEstimateLizardLizV1;
        }
        if ([methodName isEqualToString:@"lizard-fastlz4-huffman"]) {
            return kSZCompressionEstimateLizardFastLz4Huffman;
        }
        if ([methodName isEqualToString:@"lizard-lizv1-huffman"]) {
            return kSZCompressionEstimateLizardLizV1Huffman;
        }
#endif
        if ([methodName isEqualToString:@"gnu"]) {
            return kSZCompressionEstimateGnu;
        }
        if ([methodName isEqualToString:@"posix"]) {
            return kSZCompressionEstimatePosix;
        }
    }

    switch (settings.format) {
    case SZArchiveFormatGZip:
        return kSZCompressionEstimateDeflate;
    case SZArchiveFormatBZip2:
        return kSZCompressionEstimateBZip2;
    case SZArchiveFormatXz:
        return kSZCompressionEstimateLZMA2;
#if SHICHIZIP_ZS_VARIANT
    case SZArchiveFormatZstd:
        return kSZCompressionEstimateZstd;
    case SZArchiveFormatBrotli:
        return kSZCompressionEstimateBrotli;
    case SZArchiveFormatLizard:
        return kSZCompressionEstimateLizardFastLz4;
    case SZArchiveFormatLz4:
        return kSZCompressionEstimateLz4;
    case SZArchiveFormatLz5:
        return kSZCompressionEstimateLz5;
#endif
    default:
        break;
    }

    switch (settings.method) {
    case SZCompressionMethodLZMA:
        return kSZCompressionEstimateLZMA;
    case SZCompressionMethodLZMA2:
        return kSZCompressionEstimateLZMA2;
    case SZCompressionMethodPPMd:
        return settings.format == SZArchiveFormatZip ? kSZCompressionEstimatePPMdZip
                                                     : kSZCompressionEstimatePPMd;
    case SZCompressionMethodBZip2:
        return kSZCompressionEstimateBZip2;
    case SZCompressionMethodDeflate:
        return kSZCompressionEstimateDeflate;
    case SZCompressionMethodDeflate64:
        return kSZCompressionEstimateDeflate64;
    case SZCompressionMethodCopy:
        return kSZCompressionEstimateCopy;
    }

    return -1;
}

static SZCompressionEstimateRamInfo SZCompressionEstimateGetRamInfo() {
    size_t size = (size_t)sizeof(size_t) << 29;
    const bool isDefined = NWindows::NSystem::GetRamSize(size);

    if (sizeof(size_t) * 8 == 32) {
        const UInt32 limit2 = (UInt32)7 << 28;
        if (size > limit2) {
            size = limit2;
        }
    }

    const size_t kMinUseSize = (size_t)1 << 26;
    if (size < kMinUseSize) {
        size = kMinUseSize;
    }

    SZCompressionEstimateRamInfo info;
    info.IsDefined = isDefined;
    info.RamSizeReduced = size;
    info.UsageAuto = Calc_From_Val_Percents(size, 80);
    return info;
}

static bool SZCompressionEstimateGetMemoryUsageLimit(
    SZCompressionSettings* settings,
    const SZCompressionEstimateRamInfo& ramInfo, UInt64& memoryUsageLimit) {
    memoryUsageLimit = ramInfo.UsageAuto;
    bool isDefined = ramInfo.IsDefined;

    if (settings.memoryUsage.length > 0) {
        NSString* spec = [[settings.memoryUsage
            stringByTrimmingCharactersInSet:NSCharacterSet
                                                .whitespaceAndNewlineCharacterSet]
            lowercaseString];
        if (spec.length > 0) {
            UInt64 parsedLimit = 0;
            bool parsed = false;

            if ([spec hasSuffix:@"%"] && spec.length > 1) {
                NSString* valueText = [spec substringToIndex:spec.length - 1];
                unsigned long long percentValue = 0;
                NSScanner* scanner = [NSScanner scannerWithString:valueText];
                parsed =
                    [scanner scanUnsignedLongLong:&percentValue] && scanner.isAtEnd;
                if (parsed) {
                    parsedLimit = Calc_From_Val_Percents(ramInfo.RamSizeReduced,
                        (UInt64)percentValue);
                }
            } else {
                NSString* valueText = spec;
                if ([valueText hasSuffix:@"b"] && valueText.length > 1) {
                    valueText = [valueText substringToIndex:valueText.length - 1];
                }

                unsigned shift = 0;
                if (valueText.length > 1) {
                    const unichar suffix =
                        [valueText characterAtIndex:valueText.length - 1];
                    switch (suffix) {
                    case 'k':
                        shift = 10;
                        break;
                    case 'm':
                        shift = 20;
                        break;
                    case 'g':
                        shift = 30;
                        break;
                    case 't':
                        shift = 40;
                        break;
                    default:
                        break;
                    }
                    if (shift != 0) {
                        valueText = [valueText substringToIndex:valueText.length - 1];
                    }
                }

                unsigned long long baseValue = 0;
                NSScanner* scanner = [NSScanner scannerWithString:valueText];
                parsed = [scanner scanUnsignedLongLong:&baseValue] && scanner.isAtEnd;
                if (parsed) {
                    parsedLimit = (UInt64)baseValue;
                    if (shift != 0) {
                        if (parsedLimit >= ((UInt64)1 << (64 - shift))) {
                            parsed = false;
                        } else {
                            parsedLimit <<= shift;
                        }
                    }
                }
            }

            if (parsed) {
                memoryUsageLimit = parsedLimit;
                isDefined = true;
            }
        }
    }

    return isDefined;
}

static void
SZCompressionEstimateGetCpuThreadCounts(UInt32& numCPUs,
    UInt32& numHardwareThreads) {
    numCPUs = 1;
    numHardwareThreads = 1;

    NWindows::NSystem::CProcessAffinity threadsInfo;
    threadsInfo.InitST();

#ifdef _WIN32
#ifndef Z7_ST
    threadsInfo.Get_and_return_NumProcessThreads_and_SysThreads(
        numCPUs, numHardwareThreads);
#endif
#else
    if (threadsInfo.Get()) {
        numCPUs = threadsInfo.GetNumProcessThreads();
        numHardwareThreads = threadsInfo.GetNumSystemThreads();
    } else {
        numCPUs = NWindows::NSystem::GetNumberOfProcessors();
        numHardwareThreads = numCPUs;
    }

    if (numCPUs == 0) {
        numCPUs = 1;
    }
    if (numHardwareThreads < numCPUs) {
        numHardwareThreads = numCPUs;
    }
#endif
}

static UInt64 SZCompressionEstimateAutoDictionary(int methodID, int level) {
    switch (methodID) {
    case kSZCompressionEstimateLZMA:
    case kSZCompressionEstimateLZMA2:
        return level <= 4 ? (UInt64)1 << (level * 2 + 16)
            : level <= sizeof(size_t) / 2 + 4
            ? (UInt64)1 << (level + 20)
            : (UInt64)1 << (sizeof(size_t) / 2 + 24);

    case kSZCompressionEstimatePPMd:
    case kSZCompressionEstimatePPMdZip:
        return (UInt64)1 << (level + 19);

    case kSZCompressionEstimateDeflate:
        return (UInt64)1 << 15;

    case kSZCompressionEstimateDeflate64:
        return (UInt64)1 << 16;

    case kSZCompressionEstimateBZip2:
        if (level >= 5) {
            return (UInt64)900 << 10;
        }
        if (level >= 3) {
            return (UInt64)500 << 10;
        }
        return (UInt64)100 << 10;

    case kSZCompressionEstimateCopy:
        return 0;

#if SHICHIZIP_ZS_VARIANT
    case kSZCompressionEstimateFastLzma2:
        if (level == 0) {
            level = 1;
        }
        if (level > 9) {
            level = 9;
        }
        switch (level) {
        case 1:
            return (UInt64)1 << 20;
        case 2:
        case 3:
            return (UInt64)2 << 20;
        case 4:
            return (UInt64)4 << 20;
        case 5:
            return (UInt64)16 << 20;
        case 6:
            return (UInt64)32 << 20;
        case 7:
        case 8:
            return (UInt64)64 << 20;
        case 9:
        default:
            return (UInt64)128 << 20;
        }

    case kSZCompressionEstimateZstd:
        if (level == (int)kSZCompressionZstdUltimateLevel) {
            return (UInt64)1 << 27;
        }
        if (SZCompressionEstimateIsZstdFastLevel(level)) {
            const UInt32 fastLevel = SZCompressionEstimateZstdFastLevel(level);
            const UInt32 windowLog = fastLevel >= 12 ? 20u
                : fastLevel >= 6                     ? 22u
                                                     : 24u;
            return (UInt64)1 << windowLog;
        }
        if (level <= 3) {
            return (UInt64)1 << 23;
        }
        if (level <= 9) {
            return (UInt64)1 << 24;
        }
        if (level <= 16) {
            return (UInt64)1 << 25;
        }
        return (UInt64)1 << 26;

    case kSZCompressionEstimateBrotli:
        return level >= 9 ? (UInt64)16 << 20
            : level >= 5  ? (UInt64)8 << 20
                          : (UInt64)4 << 20;

    case kSZCompressionEstimateLz4:
        if (level <= 4) {
            return (UInt64)4 << 20;
        }
        if (level <= 8) {
            return (UInt64)8 << 20;
        }
        return (UInt64)16 << 20;

    case kSZCompressionEstimateLz5:
        if (level <= 5) {
            return (UInt64)8 << 20;
        }
        if (level <= 10) {
            return (UInt64)16 << 20;
        }
        return (UInt64)32 << 20;

    case kSZCompressionEstimateLizardFastLz4:
    case kSZCompressionEstimateLizardLizV1:
    case kSZCompressionEstimateLizardFastLz4Huffman:
    case kSZCompressionEstimateLizardLizV1Huffman:
        return level >= 40 ? (UInt64)64 << 20
            : level >= 30  ? (UInt64)32 << 20
                           : (UInt64)16 << 20;
#endif

    default:
        return (UInt64)-1;
    }
}

static UInt64 SZCompressionEstimateDictionary(SZCompressionSettings* settings,
    int methodID, int level) {
    if (settings.dictionarySize > 0) {
        return settings.dictionarySize;
    }
    return SZCompressionEstimateAutoDictionary(methodID, level);
}

static bool SZCompressionEstimateAutoWordSize(int methodID, int level,
    UInt32& wordSize) {
    switch (methodID) {
    case kSZCompressionEstimateLZMA:
    case kSZCompressionEstimateLZMA2:
        wordSize = (level < 7 ? 32u : 64u);
        return true;

    case kSZCompressionEstimateDeflate:
    case kSZCompressionEstimateDeflate64:
        if (level >= 9) {
            wordSize = 128;
        } else if (level >= 7) {
            wordSize = 64;
        } else {
            wordSize = 32;
        }
        return true;

    case kSZCompressionEstimatePPMd:
        if (level >= 9) {
            wordSize = 32;
        } else if (level >= 7) {
            wordSize = 16;
        } else if (level >= 5) {
            wordSize = 6;
        } else {
            wordSize = 4;
        }
        return true;

    case kSZCompressionEstimatePPMdZip:
        wordSize = level + 3;
        return true;

#if SHICHIZIP_ZS_VARIANT
    case kSZCompressionEstimateFastLzma2:
        if (level == 0) {
            level = 1;
        }
        if (level <= 4) {
            wordSize = 32;
        } else if (level == 5) {
            wordSize = 48;
        } else if (level == 6) {
            wordSize = 64;
        } else if (level == 7) {
            wordSize = 96;
        } else {
            wordSize = 273;
        }
        return true;
#endif

    default:
        return false;
    }
}

static bool SZCompressionEstimateWordSize(SZCompressionSettings* settings,
    int methodID, int level,
    UInt32& wordSize) {
    if (settings.wordSize > 0) {
        wordSize = settings.wordSize;
        return true;
    }
    return SZCompressionEstimateAutoWordSize(methodID, level, wordSize);
}

static UInt64 SZCompressionEstimateMemoryUsage_Threads_Dict_DecompMem(
    SZArchiveFormat format, int methodID, int level, UInt32 numThreads,
    UInt64 dict64, UInt64& decompressMemory) {
    decompressMemory = (UInt64)-1;

    if (level == 0) {
        decompressMemory = (UInt64)1 << 20;
        return decompressMemory;
    }

    UInt64 size = 0;
    if (SZCompressionEstimateFormatSupportsFilters(format) && level >= 9) {
        size += (12 << 20) * 2 + (5 << 20);
    }

    UInt32 numMainZipThreads = 1;
    if (SZCompressionEstimateIsZipFormat(format)) {
        UInt32 numSubThreads = 1;
        if (methodID == kSZCompressionEstimateLZMA && numThreads > 1 && level >= 5) {
            numSubThreads = 2;
        }
        numMainZipThreads = numThreads / numSubThreads;
        if (numMainZipThreads > 1) {
            size += (UInt64)numMainZipThreads * ((size_t)sizeof(size_t) << 23);
        } else {
            numMainZipThreads = 1;
        }
    }

    if (dict64 == (UInt64)-1) {
        return (UInt64)-1;
    }

    switch (methodID) {
    case kSZCompressionEstimateLZMA:
    case kSZCompressionEstimateLZMA2: {
        const UInt32 dict = (dict64 >= kSZCompressionEstimateLzmaMaxDictSize
                ? kSZCompressionEstimateLzmaMaxDictSize
                : (UInt32)dict64);

        UInt32 hashSize = dict - 1;
        hashSize |= (hashSize >> 1);
        hashSize |= (hashSize >> 2);
        hashSize |= (hashSize >> 4);
        hashSize |= (hashSize >> 8);
        hashSize >>= 1;
        if (hashSize >= (1 << 24)) {
            hashSize >>= 1;
        }
        hashSize |= (1 << 16) - 1;
        if (level < 5) {
            hashSize |= (256 << 10) - 1;
        }
        hashSize++;

        UInt64 size1 = (UInt64)hashSize * 4;
        size1 += (UInt64)dict * 4;
        if (level >= 5) {
            size1 += (UInt64)dict * 4;
        }
        size1 += (2 << 20);

        UInt32 numThreads1 = 1;
        if (numThreads > 1 && level >= 5) {
            size1 += (2 << 20) + (4 << 20);
            numThreads1 = 2;
        }

        UInt32 numBlockThreads = numThreads / numThreads1;
        UInt64 chunkSize = 0;
        if (methodID == kSZCompressionEstimateLZMA2 && numBlockThreads != 1) {
            chunkSize = (UInt64)dict << 2;
            const UInt32 kMinSize = (UInt32)1 << 20;
            const UInt32 kMaxSize = (UInt32)1 << 28;
            if (chunkSize < kMinSize) {
                chunkSize = kMinSize;
            }
            if (chunkSize > kMaxSize) {
                chunkSize = kMaxSize;
            }
            if (chunkSize < dict) {
                chunkSize = dict;
            }
            chunkSize += (kMinSize - 1);
            chunkSize &= ~(UInt64)(kMinSize - 1);
        }

        if (chunkSize == 0) {
            const UInt32 kBlockSizeMax = (UInt32)0 - (UInt32)(1 << 16);
            UInt64 blockSize = (UInt64)dict + (1 << 16) + (numThreads1 > 1 ? (1 << 20) : 0);
            blockSize += (blockSize >> (blockSize < ((UInt32)1 << 30) ? 1 : 2));
            if (blockSize >= kBlockSizeMax) {
                blockSize = kBlockSizeMax;
            }
            size += numBlockThreads * (size1 + blockSize);
        } else {
            size += numBlockThreads * (size1 + chunkSize);
            const UInt32 numPackChunks = numBlockThreads + (numBlockThreads / 8) + 1;
            if (chunkSize < ((UInt32)1 << 26)) {
                numBlockThreads++;
            }
            if (chunkSize < ((UInt32)1 << 24)) {
                numBlockThreads++;
            }
            if (chunkSize < ((UInt32)1 << 22)) {
                numBlockThreads++;
            }
            size += numPackChunks * chunkSize;
        }

        decompressMemory = dict + (2 << 20);
        return size;
    }

    case kSZCompressionEstimatePPMd:
        decompressMemory = dict64 + (2 << 20);
        return size + decompressMemory;

    case kSZCompressionEstimateDeflate:
    case kSZCompressionEstimateDeflate64: {
        UInt64 size1 = 3 << 20;
        size1 += (1 << 20);
        size += size1 * numMainZipThreads;
        decompressMemory = (2 << 20);
        return size;
    }

    case kSZCompressionEstimateBZip2:
        decompressMemory = (7 << 20);
        return size + ((UInt64)10 << 20) * numThreads;

    case kSZCompressionEstimatePPMdZip:
        decompressMemory = dict64 + (2 << 20);
        return size + (UInt64)decompressMemory * numThreads;

#if SHICHIZIP_ZS_VARIANT
    case kSZCompressionEstimateFastLzma2: {
        const UInt64 dictionary = dict64 == (UInt64)-1 ? (UInt64)16 << 20 : dict64;
        const UInt32 effectiveThreads = numThreads == 0 ? 1u : numThreads;
        decompressMemory = dictionary + (4 << 20);
        return ((UInt64)12 << 20) + dictionary * 3 + ((UInt64)4 << 20) * effectiveThreads;
    }

    case kSZCompressionEstimateZstd: {
        const UInt64 dictionary = dict64 == (UInt64)-1 ? (UInt64)1 << 24 : dict64;
        const UInt32 effectiveThreads = numThreads == 0 ? 1u : numThreads;
        const UInt64 perThread = SZCompressionEstimateIsZstdFastLevel(level)
            ? (UInt64)3 << 20
            : (UInt64)8 << 20;
        decompressMemory = dictionary + (2 << 20);
        return dictionary * 2 + perThread * effectiveThreads;
    }

    case kSZCompressionEstimateBrotli: {
        const UInt64 dictionary = dict64 == (UInt64)-1 ? (UInt64)8 << 20 : dict64;
        const UInt32 effectiveThreads = numThreads == 0 ? 1u : numThreads;
        decompressMemory = dictionary + (1 << 20);
        return dictionary * 2 + ((UInt64)10 << 20) * effectiveThreads;
    }

    case kSZCompressionEstimateLz4: {
        const UInt64 dictionary = dict64 == (UInt64)-1 ? (UInt64)8 << 20 : dict64;
        const UInt32 effectiveThreads = numThreads == 0 ? 1u : numThreads;
        decompressMemory = (4 << 20);
        return dictionary + ((UInt64)6 << 20) * effectiveThreads;
    }

    case kSZCompressionEstimateLz5: {
        const UInt64 dictionary = dict64 == (UInt64)-1 ? (UInt64)16 << 20 : dict64;
        const UInt32 effectiveThreads = numThreads == 0 ? 1u : numThreads;
        decompressMemory = (6 << 20);
        return dictionary + ((UInt64)8 << 20) * effectiveThreads;
    }

    case kSZCompressionEstimateLizardFastLz4:
    case kSZCompressionEstimateLizardLizV1:
    case kSZCompressionEstimateLizardFastLz4Huffman:
    case kSZCompressionEstimateLizardLizV1Huffman: {
        const UInt64 dictionary = dict64 == (UInt64)-1 ? (UInt64)16 << 20 : dict64;
        const UInt32 effectiveThreads = numThreads == 0 ? 1u : numThreads;
        decompressMemory = (8 << 20);
        return dictionary + ((UInt64)10 << 20) * effectiveThreads;
    }
#endif

    default:
        return (UInt64)-1;
    }
}

static UInt32 SZCompressionEstimateAutoThreads(SZCompressionSettings* settings,
    int methodID, int level,
    UInt64 dict64,
    UInt64 memoryUsageLimit,
    bool memoryUsageLimitIsDefined) {
    if (!SZCompressionEstimateFormatSupportsThreads(settings.format)) {
        return 1;
    }

    UInt32 numCPUs = 1;
    UInt32 numHardwareThreads = 1;
    SZCompressionEstimateGetCpuThreadCounts(numCPUs, numHardwareThreads);

    UInt32 numAlgoThreadsMax = numHardwareThreads * 2;
    if (SZCompressionEstimateIsZipFormat(settings.format)) {
        numAlgoThreadsMax = 8 << (sizeof(size_t) / 2);
    } else if (SZCompressionEstimateIsXzFormat(settings.format)) {
        numAlgoThreadsMax = 256 * 2;
    } else {
        switch (methodID) {
        case kSZCompressionEstimateLZMA:
            numAlgoThreadsMax = 2;
            break;
        case kSZCompressionEstimateLZMA2:
            numAlgoThreadsMax = 256 * 2;
            break;
        case kSZCompressionEstimateBZip2:
            numAlgoThreadsMax = 64;
            break;
#if SHICHIZIP_ZS_VARIANT
        case kSZCompressionEstimateFastLzma2:
        case kSZCompressionEstimateZstd:
        case kSZCompressionEstimateBrotli:
        case kSZCompressionEstimateLz4:
        case kSZCompressionEstimateLz5:
        case kSZCompressionEstimateLizardFastLz4:
        case kSZCompressionEstimateLizardLizV1:
        case kSZCompressionEstimateLizardFastLz4Huffman:
        case kSZCompressionEstimateLizardLizV1Huffman:
            numAlgoThreadsMax = 128;
            break;
#endif
        case kSZCompressionEstimateCopy:
        case kSZCompressionEstimatePPMd:
        case kSZCompressionEstimateDeflate:
        case kSZCompressionEstimateDeflate64:
        case kSZCompressionEstimatePPMdZip:
            numAlgoThreadsMax = 1;
            break;
        default:
            break;
        }
    }

    UInt32 autoThreads = numCPUs;
    if (autoThreads > numAlgoThreadsMax) {
        autoThreads = numAlgoThreadsMax;
    }

    if (memoryUsageLimitIsDefined && autoThreads > 1) {
        if (SZCompressionEstimateIsZipFormat(settings.format)) {
            for (; autoThreads > 1; autoThreads--) {
                UInt64 decompressMemory;
                const UInt64 usage = SZCompressionEstimateMemoryUsage_Threads_Dict_DecompMem(
                    settings.format, methodID, level, autoThreads, dict64,
                    decompressMemory);
                if (usage <= memoryUsageLimit) {
                    break;
                }
            }
        } else if (methodID == kSZCompressionEstimateLZMA2) {
            const UInt32 numThreads1 = (level >= 5 ? 2 : 1);
            UInt32 numBlockThreads = autoThreads / numThreads1;
            for (; numBlockThreads > 1; numBlockThreads--) {
                autoThreads = numBlockThreads * numThreads1;
                UInt64 decompressMemory;
                const UInt64 usage = SZCompressionEstimateMemoryUsage_Threads_Dict_DecompMem(
                    settings.format, methodID, level, autoThreads, dict64,
                    decompressMemory);
                if (usage <= memoryUsageLimit) {
                    break;
                }
            }
            autoThreads = numBlockThreads * numThreads1;
        }
    }

    return autoThreads;
}

} // namespace

// ============================================================
// SZArchive — main class
// ============================================================

@interface SZArchive () {
    std::unique_ptr<CArchiveLink> _arcLink;
    BOOL _isOpen;
    NSString* _archivePath;
    NSString* _openType;
    NSString* _cachedPassword;
    BOOL _cachedPasswordIsDefined;
}
@end

struct SZEntryPropertyMetadata {
    const char* key;
    const char* titleKey;
    const char* title;
};

static SZEntryPropertyMetadata SZEntryPropertyMetadataForPropID(PROPID propID) {
    switch (propID) {
    case kpidPath:
    case kpidName:
        return { "name", "column.name", "Name" };
    case kpidExtension:
        return { "extension", "column.extension", "Extension" };
    case kpidIsDir:
        return { "folder", "column.folder", "Folder" };
    case kpidSize:
        return { "size", "column.size", "Size" };
    case kpidPackSize:
        return { "packedSize", "column.packedSize", "Packed Size" };
    case kpidMTime:
        return { "modified", "column.modified", "Modified" };
    case kpidCTime:
        return { "created", "column.created", "Created" };
    case kpidATime:
        return { "accessed", "column.accessed", "Accessed" };
    case kpidAttrib:
        return { "attributes", "column.attributes", "Attributes" };
    case kpidSolid:
        return { "solid", "column.solid", "Solid" };
    case kpidCommented:
        return { "commented", "column.commented", "Commented" };
    case kpidEncrypted:
        return { "encrypted", "column.encrypted", "Encrypted" };
    case kpidSplitBefore:
        return { "splitBefore", "column.splitBefore", "Split Before" };
    case kpidSplitAfter:
        return { "splitAfter", "column.splitAfter", "Split After" };
    case kpidDictionarySize:
        return { "dictionary", "column.dictionary", "Dictionary" };
    case kpidCRC:
        return { "crc", "column.crc", "CRC" };
    case kpidType:
        return { "type", "column.type", "Type" };
    case kpidIsAnti:
        return { "anti", "column.anti", "Anti" };
    case kpidMethod:
        return { "method", "column.method", "Method" };
    case kpidHostOS:
        return { "hostOS", "column.hostOS", "Host OS" };
    case kpidFileSystem:
        return { "fileSystem", "column.fileSystem", "File System" };
    case kpidUser:
        return { "user", "column.user", "User" };
    case kpidGroup:
        return { "group", "column.group", "Group" };
    case kpidBlock:
        return { "block", "column.block", "Block" };
    case kpidComment:
        return { "comment", "column.comment", "Comment" };
    case kpidPosition:
        return { "position", "column.position", "Position" };
    case kpidPrefix:
        return { "pathPrefix", "column.pathPrefix", "Path Prefix" };
    case kpidNumSubDirs:
        return { "folders", "column.folders", "Folders" };
    case kpidNumSubFiles:
        return { "files", "column.files", "Files" };
    case kpidUnpackVer:
        return { "version", "column.version", "Version" };
    case kpidVolume:
        return { "volume", "column.volume", "Volume" };
    case kpidIsVolume:
        return { "multivolume", "column.multivolume", "Multivolume" };
    case kpidOffset:
        return { "offset", "column.offset", "Offset" };
    case kpidLinks:
        return { "links", "column.links", "Links" };
    case kpidNumBlocks:
        return { "blocks", "column.blocks", "Blocks" };
    case kpidNumVolumes:
        return { "volumes", "column.volumes", "Volumes" };
    case kpidTimeType:
        return { "timeType", "column.timeType", "Time Type" };
    case kpidBit64:
        return { "bit64", "column.bit64", "64-bit" };
    case kpidBigEndian:
        return { "bigEndian", "column.bigEndian", "Big-endian" };
    case kpidCpu:
        return { "cpu", "column.cpu", "CPU" };
    case kpidPhySize:
        return { "physicalSize", "column.physicalSize", "Physical Size" };
    case kpidHeadersSize:
        return { "headersSize", "column.headersSize", "Headers Size" };
    case kpidChecksum:
        return { "checksum", "column.checksum", "Checksum" };
    case kpidCharacts:
        return { "characteristics", "column.characteristics", "Characteristics" };
    case kpidVa:
        return { "virtualAddress", "column.virtualAddress", "Virtual Address" };
    case kpidId:
        return { "id", "column.id", "ID" };
    case kpidShortName:
        return { "shortName", "column.shortName", "Short Name" };
    case kpidCreatorApp:
        return { "creatorApplication", "column.creatorApplication", "Creator Application" };
    case kpidSectorSize:
        return { "sectorSize", "column.sectorSize", "Sector Size" };
    case kpidPosixAttrib:
        return { "posixAttributes", "column.posixAttributes", "Mode" };
    case kpidSymLink:
        return { "symbolicLink", "column.symbolicLink", "Symbolic Link" };
    case kpidError:
        return { "error", "column.error", "Error" };
    case kpidTotalSize:
        return { "totalSize", "column.totalSize", "Total Size" };
    case kpidFreeSpace:
        return { "freeSpace", "column.freeSpace", "Free Space" };
    case kpidClusterSize:
        return { "clusterSize", "column.clusterSize", "Cluster Size" };
    case kpidVolumeName:
        return { "volumeName", "column.volumeName", "Label" };
    case kpidLocalName:
        return { "localName", "column.localName", "Local Name" };
    case kpidProvider:
        return { "provider", "column.provider", "Provider" };
    case kpidNtSecure:
        return { "ntSecurity", "column.ntSecurity", "NT Security" };
    case kpidIsAltStream:
        return { "alternateStream", "column.alternateStream", "Alternate Stream" };
    case kpidIsAux:
        return { "aux", "column.aux", "Aux" };
    case kpidIsDeleted:
        return { "deleted", "column.deleted", "Deleted" };
    case kpidIsTree:
        return { "tree", "column.tree", "Is Tree" };
    case kpidSha1:
        return { "sha1", "column.sha1", "SHA-1" };
    case kpidSha256:
        return { "sha256", "column.sha256", "SHA-256" };
    case kpidErrorType:
        return { "errorType", "column.errorType", "Error Type" };
    case kpidNumErrors:
        return { "errors", "column.errors", "Errors" };
    case kpidErrorFlags:
        return { "errorFlags", "column.errorFlags", "Errors" };
    case kpidWarningFlags:
        return { "warningFlags", "column.warningFlags", "Warnings" };
    case kpidWarning:
        return { "warning", "column.warning", "Warning" };
    case kpidNumStreams:
        return { "streams", "column.streams", "Streams" };
    case kpidNumAltStreams:
        return { "alternateStreams", "column.alternateStreams", "Alternate Streams" };
    case kpidAltStreamsSize:
        return { "alternateStreamsSize", "column.alternateStreamsSize", "Alternate Streams Size" };
    case kpidVirtualSize:
        return { "virtualSize", "column.virtualSize", "Virtual Size" };
    case kpidUnpackSize:
        return { "unpackSize", "column.unpackSize", "Unpack Size" };
    case kpidTotalPhySize:
        return { "totalPhysicalSize", "column.totalPhysicalSize", "Total Physical Size" };
    case kpidVolumeIndex:
        return { "volumeIndex", "column.volumeIndex", "Volume Index" };
    case kpidSubType:
        return { "subtype", "column.subtype", "SubType" };
    case kpidShortComment:
        return { "shortComment", "column.shortComment", "Short Comment" };
    case kpidCodePage:
        return { "codePage", "column.codePage", "Code Page" };
    case kpidIsNotArcType:
        return { "notArchiveType", "column.notArchiveType", "Is not archive type" };
    case kpidPhySizeCantBeDetected:
        return { "physicalSizeCannotBeDetected", "column.physicalSizeCannotBeDetected", "Physical Size can't be detected" };
    case kpidZerosTailIsAllowed:
        return { "zerosTailIsAllowed", "column.zerosTailIsAllowed", "Zeros Tail Is Allowed" };
    case kpidTailSize:
        return { "tailSize", "column.tailSize", "Tail Size" };
    case kpidEmbeddedStubSize:
        return { "embeddedStubSize", "column.embeddedStubSize", "Embedded Stub Size" };
    case kpidNtReparse:
        return { "link", "column.link", "Link" };
    case kpidHardLink:
        return { "hardLink", "column.hardLink", "Hard Link" };
    case kpidINode:
        return { "inode", "column.inode", "iNode" };
    case kpidStreamId:
        return { "streamID", "column.streamID", "Stream ID" };
    case kpidReadOnly:
        return { "readOnly", "column.readOnly", "Read-only" };
    case kpidOutName:
        return { "outName", "column.outName", "Out Name" };
    case kpidCopyLink:
        return { "copyLink", "column.copyLink", "Copy Link" };
    case kpidArcFileName:
        return { "archiveFileName", "column.archiveFileName", "ArcFileName" };
    case kpidIsHash:
        return { "hash", "column.hash", "IsHash" };
    case kpidChangeTime:
        return { "changed", "column.changed", "Metadata Changed" };
    case kpidUserId:
        return { "userID", "column.userID", "User ID" };
    case kpidGroupId:
        return { "groupID", "column.groupID", "Group ID" };
    case kpidDeviceMajor:
        return { "deviceMajor", "column.deviceMajor", "Device Major" };
    case kpidDeviceMinor:
        return { "deviceMinor", "column.deviceMinor", "Device Minor" };
    case kpidDevMajor:
        return { "devMajor", "column.devMajor", "Dev Major" };
    case kpidDevMinor:
        return { "devMinor", "column.devMinor", "Dev Minor" };
    default:
        return { nullptr, nullptr, nullptr };
    }
}

static SZArchiveEntryProperty* SZMakeEntryProperty(PROPID propID, VARTYPE valueType, NSString* propertyName) {
    const SZEntryPropertyMetadata metadata = SZEntryPropertyMetadataForPropID(propID);
    SZArchiveEntryProperty* property = [SZArchiveEntryProperty new];
    property.key = metadata.key ? @(metadata.key) : [NSString stringWithFormat:@"property.%u", (unsigned)propID];
    property.titleKey = metadata.titleKey ? @(metadata.titleKey) : nil;
    if (metadata.title) {
        property.title = @(metadata.title);
    } else if (propertyName.length > 0) {
        property.title = propertyName;
    } else {
        property.title = [NSString stringWithFormat:@"Property %u", (unsigned)propID];
    }
    property.propID = (NSUInteger)propID;
    property.valueType = (NSUInteger)valueType;
    return property;
}

static NSArray<SZArchiveEntryProperty*>* SZCopyEntryProperties(IInArchive* archive) {
    NSMutableArray<SZArchiveEntryProperty*>* properties = [NSMutableArray array];
    NSMutableSet<NSString*>* seen = [NSMutableSet set];

    void (^addProperty)(SZArchiveEntryProperty*) = ^(SZArchiveEntryProperty* property) {
        if (property.key.length == 0 || [seen containsObject:property.key])
            return;
        [seen addObject:property.key];
        [properties addObject:property];
    };

    addProperty(SZMakeEntryProperty(kpidPath, VT_BSTR, nil));

    UInt32 propertyCount = 0;
    if (archive->GetNumberOfProperties(&propertyCount) != S_OK)
        return properties;

    for (UInt32 propertyIndex = 0; propertyIndex < propertyCount; propertyIndex++) {
        BSTR propertyNameBSTR = NULL;
        PROPID propID = kpidNoProperty;
        VARTYPE valueType = VT_EMPTY;
        const HRESULT result = archive->GetPropertyInfo(propertyIndex, &propertyNameBSTR, &propID, &valueType);
        NSString* propertyName = propertyNameBSTR ? ToNS(UString(propertyNameBSTR)) : @"";
        if (propertyNameBSTR)
            ::SysFreeString(propertyNameBSTR);
        if (result != S_OK || propID == kpidIsDir)
            continue;
        addProperty(SZMakeEntryProperty(propID, valueType, propertyName));
    }

    return properties;
}

static NSString* SZPropertyDisplayString(IInArchive* archive, UInt32 itemIndex, PROPID propID) {
    NWindows::NCOM::CPropVariant value;
    if (archive->GetProperty(itemIndex, propID, &value) != S_OK || value.vt == VT_EMPTY)
        return nil;

    UString displayString;
    ConvertPropertyToString2(displayString, value, propID);
    if (displayString.IsEmpty())
        return @"";
    return ToNS(displayString);
}

static BOOL SZOpenErrorFlagsIndicateWrongPassword(UInt32 errorFlags) {
    return (errorFlags & kpv_ErrorFlags_EncryptedHeadersError) != 0;
}

static BOOL SZOpenErrorFlagsIndicateUnsupportedArchive(UInt32 errorFlags) {
    return (errorFlags & (kpv_ErrorFlags_IsNotArc | kpv_ErrorFlags_UnsupportedMethod | kpv_ErrorFlags_UnsupportedFeature)) != 0;
}

static NSString* SZOpenArchiveFlagDetails(UInt32 errorFlags) {
    NSMutableArray<NSString*>* messages = [NSMutableArray array];

    const struct {
        UInt32 flag;
        NSString* key;
    } flagMessages[] = {
        { kpv_ErrorFlags_IsNotArc, @"error.isNotArchive" },
        { kpv_ErrorFlags_HeadersError, @"error.headersError" },
        { kpv_ErrorFlags_EncryptedHeadersError, @"error.headersError" },
        { kpv_ErrorFlags_UnavailableStart, @"error.unavailableStart" },
        { kpv_ErrorFlags_UnconfirmedStart, @"error.unconfirmedStart" },
        { kpv_ErrorFlags_UnexpectedEnd, @"error.unexpectedEnd" },
        { kpv_ErrorFlags_DataAfterEnd, @"error.dataAfterPayload" },
        { kpv_ErrorFlags_UnsupportedMethod, @"error.unsupportedMethodGeneric" },
        { kpv_ErrorFlags_UnsupportedFeature, @"error.unsupportedFeature" },
        { kpv_ErrorFlags_DataError, @"error.dataErrorGeneric" },
        { kpv_ErrorFlags_CrcError, @"error.crcFailedGeneric" },
    };

    for (size_t index = 0; index < sizeof(flagMessages) / sizeof(flagMessages[0]);
        index++) {
        const auto& entry = flagMessages[index];
        if ((errorFlags & entry.flag) == 0) {
            continue;
        }
        if (entry.flag == kpv_ErrorFlags_EncryptedHeadersError) {
            [messages addObject:[NSString stringWithFormat:@"%@ : %@",
                                    SZLocalizedString(entry.key),
                                    SZLocalizedString(@"error.wrongPassword")]];
        } else {
            [messages addObject:SZLocalizedString(entry.key)];
        }
    }

    return messages.count > 0 ? [messages componentsJoinedByString:@"\n"] : nil;
}

static NSString* SZOpenArchiveFailureReason(const CArcErrorInfo& errorInfo) {
    NSMutableArray<NSString*>* messages = [NSMutableArray array];

    NSString* flagDetails = SZOpenArchiveFlagDetails(errorInfo.GetErrorFlags());
    if (flagDetails.length > 0) {
        [messages addObject:flagDetails];
    }

    NSString* errorMessage = ToNS(errorInfo.ErrorMessage);
    if (errorMessage.length > 0 && ![messages containsObject:errorMessage]) {
        [messages addObject:errorMessage];
    }

    return messages.count > 0 ? [messages componentsJoinedByString:@"\n"] : nil;
}

static NSError* SZOpenArchiveErrorFromPasswordContext(
    HRESULT result, const CArcErrorInfo& errorInfo, BOOL passwordWasAsked,
    NSString* archivePath) {
    if (result == E_ABORT) {
        return SZMakeError(SZArchiveErrorCodeUserCancelled,
            SZLocalizedString(@"app.archive.error.operationCancelled"));
    }

    if (result != S_FALSE) {
        return SZMakeError(
            result, [NSString stringWithFormat:SZLocalizedString(@"app.archive.error.failedToOpenFormat"), (unsigned)result]);
    }

    const UInt32 errorFlags = errorInfo.GetErrorFlags();
    const BOOL wrongPassword = passwordWasAsked
        || SZOpenErrorFlagsIndicateWrongPassword(errorFlags);
    if (wrongPassword) {
        return SZMakeDetailedError(SZArchiveErrorCodeWrongPassword,
            SZLocalizedStringWithFirstPlaceholder(@"archive.cannotOpenEncryptedWrongPassword", archivePath),
            SZOpenArchiveFailureReason(errorInfo));
    }

    if (SZOpenErrorFlagsIndicateUnsupportedArchive(errorFlags)) {
        return SZMakeDetailedError(SZArchiveErrorCodeUnsupportedArchive,
            SZLocalizedString(@"archive.unsupportedType"),
            SZOpenArchiveFailureReason(errorInfo));
    }

    if (!errorInfo.IsArc_After_NonOpen() && errorInfo.ErrorMessage.IsEmpty()) {
        return SZMakeDetailedError(SZArchiveErrorCodeUnsupportedArchive,
            SZLocalizedString(@"archive.unsupportedType"),
            SZOpenArchiveFailureReason(errorInfo));
    }

    return SZMakeDetailedError(SZArchiveErrorCodeInvalidArchive,
        SZLocalizedStringWithFirstPlaceholder(@"archive.cannotOpenFileAsArchive", archivePath),
        SZOpenArchiveFailureReason(errorInfo));
}

static NSError*
SZOpenArchiveErrorFromResult(HRESULT result, const CArcErrorInfo& errorInfo,
    const SZOpenCallbackUI& callbackUI,
    NSString* archivePath) {
    return SZOpenArchiveErrorFromPasswordContext(result, errorInfo,
        callbackUI.PasswordWasAsked, archivePath);
}

static NSString* SZNormalizeArchiveRelativePath(NSString* path) {
    NSString* normalized = [path copy] ?: @"";
    while (normalized.length > 0 && [normalized hasSuffix:@"/"]) {
        normalized = [normalized substringToIndex:normalized.length - 1];
    }
    return normalized;
}

static NSArray<NSString*>* SZArchivePathComponents(NSString* path) {
    NSString* normalized = SZNormalizeArchiveRelativePath(path);
    if (normalized.length == 0) {
        return @[];
    }

    NSMutableArray<NSString*>* components = [NSMutableArray array];
    for (NSString* component in [normalized componentsSeparatedByString:@"/"]) {
        if (component.length > 0) {
            [components addObject:component];
        }
    }
    return components;
}

static NSString* SZArchiveParentPath(NSString* path) {
    NSArray<NSString*>* components = SZArchivePathComponents(path);
    if (components.count <= 1) {
        return @"";
    }
    return [[components subarrayWithRange:NSMakeRange(0, components.count - 1)]
        componentsJoinedByString:@"/"];
}

static NSString* SZArchiveLeafName(NSString* path) {
    return SZArchivePathComponents(path).lastObject ?: @"";
}

static NSString* SZFolderItemName(IFolderFolder* folder, UInt32 index) {
    NWindows::NCOM::CPropVariant value;
    if (folder->GetProperty(index, kpidName, &value) != S_OK) {
        return nil;
    }
    if (value.vt == VT_BSTR && value.bstrVal) {
        return ToNS(UString(value.bstrVal));
    }
    return nil;
}

static HRESULT SZBindFolderToArchiveSubdir(IFolderFolder* rootFolder,
    NSString* archiveSubdir,
    IFolderFolder** resultFolder) {
    CMyComPtr<IFolderFolder> currentFolder = rootFolder;
    for (NSString* component in SZArchivePathComponents(archiveSubdir)) {
        CMyComPtr<IFolderFolder> nextFolder;
        RINOK(currentFolder->BindToFolder(ToU(component), &nextFolder))
        if (!nextFolder) {
            return E_INVALIDARG;
        }
        currentFolder = nextFolder;
    }

    *resultFolder = currentFolder.Detach();
    return S_OK;
}

static HRESULT SZResolveFolderItemIndices(IFolderFolder* folder,
    NSArray<NSString*>* itemPaths,
    NSString* archiveSubdir,
    std::vector<UInt32>& indices) {
    indices.clear();

    NSString* normalizedSubdir = SZNormalizeArchiveRelativePath(archiveSubdir);
    UInt32 numItems = 0;
    RINOK(folder->GetNumberOfItems(&numItems))

    for (NSString* itemPath in itemPaths) {
        NSString* normalizedPath = SZNormalizeArchiveRelativePath(itemPath);
        if (![SZArchiveParentPath(normalizedPath)
                isEqualToString:normalizedSubdir]) {
            return E_INVALIDARG;
        }

        NSString* expectedName = SZArchiveLeafName(normalizedPath);
        if (expectedName.length == 0) {
            return E_INVALIDARG;
        }

        BOOL found = NO;
        for (UInt32 index = 0; index < numItems; index++) {
            NSString* itemName = SZFolderItemName(folder, index);
            if (![itemName isEqualToString:expectedName]) {
                continue;
            }
            indices.push_back(index);
            found = YES;
            break;
        }

        if (!found) {
            return E_INVALIDARG;
        }
    }

    return S_OK;
}

static HRESULT SZOpenAgentFolder(NSString* archivePath, NSString* openType,
    SZAgentUpdateCallback* callback,
    NSString* archiveSubdir,
    CMyComPtr<IInFolderArchive>& agentOut,
    CAgent*& agentSpecOut,
    CMyComPtr<IFolderFolder>& folderOut) {
    CAgent* agentSpec = new CAgent();
    agentOut = agentSpec;
    agentSpecOut = agentSpec;

    UString openTypeText = openType.length > 0 ? ToU(openType) : UString();
    const wchar_t* arcFormat = openTypeText.IsEmpty() ? L"" : openTypeText.Ptr();
    RINOK(agentOut->Open(NULL, ToU(archivePath), arcFormat, NULL, callback))

    CMyComPtr<IFolderFolder> rootFolder;
    RINOK(agentOut->BindToRootFolder(&rootFolder))

    if (archiveSubdir.length == 0) {
        folderOut = rootFolder;
        return S_OK;
    }

    return SZBindFolderToArchiveSubdir(rootFolder, archiveSubdir, &folderOut);
}

static NSError* SZArchiveUpdateErrorFromResult(HRESULT result,
    NSString* fallbackDescription,
    const UString& errorMessage) {
    if (result == E_ABORT) {
        return SZMakeError(SZArchiveErrorCodeUserCancelled,
            SZLocalizedString(@"app.archive.error.operationCancelled"));
    }

    if (result == E_NOTIMPL) {
        return SZMakeError(
            SZArchiveErrorCodeUnsupportedFormat,
            SZLocalizedString(@"archive.updateUnsupported"));
    }

    if (result == E_INVALIDARG) {
        return SZMakeError(result, SZLocalizedString(@"app.archive.error.invalidItemSelection"));
    }

    if (result == (HRESULT)ERROR_ALREADY_EXISTS || result == HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS)) {
        return SZMakeError(
            result, SZLocalizedString(@"app.archive.error.itemAlreadyExists"));
    }

    NSString* details = ToNS(errorMessage);
    if (details.length > 0) {
        return SZMakeDetailedError(result, fallbackDescription, details);
    }

    return SZMakeError(result, [NSString stringWithFormat:@"%@ (0x%08X)", fallbackDescription, (unsigned)result]);
}

@implementation SZArchive

+ (SZCompressionResourceInfo*)compressionResourceEstimateForSettings:
    (SZCompressionSettings*)settings {
    SZCompressionResourceInfo* info = [SZCompressionResourceInfo new];
    if (!settings) {
        return info;
    }

    const int methodID = SZCompressionEstimateMethodID(settings);
    if (methodID < 0) {
        return info;
    }

    const int level = SZCompressionEstimateLevel(settings);
    const UInt64 dict64 = SZCompressionEstimateDictionary(settings, methodID, level);
    if (dict64 != (UInt64)-1) {
        info.resolvedDictionarySizeIsDefined = YES;
        info.resolvedDictionarySize = dict64;
    }

    UInt32 wordSize = 0;
    if (SZCompressionEstimateWordSize(settings, methodID, level, wordSize)) {
        info.resolvedWordSizeIsDefined = YES;
        info.resolvedWordSize = wordSize;
    }

    const SZCompressionEstimateRamInfo ramInfo = SZCompressionEstimateGetRamInfo();
    UInt64 memoryUsageLimit = ramInfo.UsageAuto;
    const bool memoryUsageLimitIsDefined = SZCompressionEstimateGetMemoryUsageLimit(settings, ramInfo,
        memoryUsageLimit);

    UInt32 numThreads = settings.numThreads;
    if (!SZCompressionEstimateFormatSupportsThreads(settings.format)) {
        numThreads = 1;
    } else if (numThreads == 0) {
        numThreads = SZCompressionEstimateAutoThreads(settings, methodID, level,
            dict64, memoryUsageLimit,
            memoryUsageLimitIsDefined);
    }
    if (SZCompressionEstimateFormatSupportsThreads(settings.format)) {
        info.resolvedNumThreadsIsDefined = YES;
        info.resolvedNumThreads = numThreads;
    }

    if (!SZCompressionEstimateFormatSupportsMemoryUse(settings.format)) {
        return info;
    }

    UInt64 decompressionMemory;
    const UInt64 compressionMemory = SZCompressionEstimateMemoryUsage_Threads_Dict_DecompMem(
        settings.format, methodID, level, numThreads, dict64,
        decompressionMemory);
    if (compressionMemory != (UInt64)-1) {
        info.compressionMemoryIsDefined = YES;
        info.compressionMemory = compressionMemory;
    }
    if (decompressionMemory != (UInt64)-1) {
        info.decompressionMemoryIsDefined = YES;
        info.decompressionMemory = decompressionMemory;
    }
    if (memoryUsageLimitIsDefined) {
        info.memoryUsageLimitIsDefined = YES;
        info.memoryUsageLimit = memoryUsageLimit;
    }
    return info;
}

- (void)clearCachedPassword {
    _cachedPassword = nil;
    _cachedPasswordIsDefined = NO;
}

- (void)storeCachedPassword:(const UString&)password defined:(bool)isDefined {
    if (isDefined) {
        _cachedPassword = ToNS(password);
        _cachedPasswordIsDefined = YES;
    } else {
        [self clearCachedPassword];
    }
}

// Keep the cached password in sync after an in-place mutation.
- (void)syncCachedPasswordFromUpdateCallback:(SZAgentUpdateCallback*)callback
                                      result:(HRESULT)result {
    if (result != S_OK && !callback->ArchiveWasReplaced) {
        return;
    }
    if (callback->PasswordIsDefined) {
        [self storeCachedPassword:callback->Password defined:true];
    } else {
        [self clearCachedPassword];
    }
}

- (void)configureExtractPasswordForCallback:(SZFolderExtractCallback*)callback
                           explicitPassword:(NSString*)password {
    if (password) {
        callback->PasswordIsDefined = true;
        callback->Password = ToU(password);
        return;
    }

    if (_cachedPasswordIsDefined) {
        callback->PasswordIsDefined = true;
        callback->Password = ToU(_cachedPassword ?: @"");
    }
}

- (void)updateCachedPasswordFromExtractCallback:
            (SZFolderExtractCallback*)callback
                                         result:(HRESULT)result {
    if (result == S_OK && callback->PasswordIsDefined) {
        [self storeCachedPassword:callback->Password defined:true];
        return;
    }

    if (callback->PasswordWasWrong || (callback->PasswordWasAsked && !callback->PasswordIsDefined)) {
        [self clearCachedPassword];
    }
}

- (BOOL)reopenAfterExternalMutationWithSession:(SZOperationSession*)session
                                         error:(NSError**)error {
    SZArchiveOperationGuard operationGuard(self);

    NSString* archivePath = [_archivePath copy];
    NSString* openType = [_openType copy];
    NSString* password = _cachedPasswordIsDefined ? [_cachedPassword copy] : nil;
    if (archivePath.length == 0) {
        if (error) {
            *error = SZMakeError(SZArchiveErrorCodeNoOpenArchive,
                SZLocalizedString(@"app.fileManager.error.noArchiveOpen"));
        }
        return NO;
    }

    [self close];
    return [self openAtPath:archivePath
                   openType:openType
                   password:password
                    session:session
                      error:error];
}

- (instancetype)init {
    if ((self = [super init])) {
        _arcLink = std::make_unique<CArchiveLink>();
        _isOpen = NO;
        _cachedPasswordIsDefined = NO;
    }
    return self;
}

- (void)dealloc {
    // Close early so upstream objects release before unique_ptr teardown.
    @try {
        [self close];
    } @catch (NSException* exception) {
        SZLogError(@"ShichiZip", @"SZArchive dealloc caught ObjC exception during close: %@", exception);
    }
    try {
        if (_arcLink) {
            _arcLink->Close();
        }
    } catch (...) {
        SZLogError(@"ShichiZip", @"SZArchive dealloc caught C++ exception during CArchiveLink::Close");
    }
}

+ (NSString*)sevenZipVersionString {
    return @MY_VERSION;
}

+ (NSString*)correctedFileSystemRelativePathForArchivePath:(NSString*)path
                                               isDirectory:(BOOL)isDirectory {
    UStringVector pathParts;
    SplitPathToParts(ToU(path), pathParts);
    Correct_FsPath(false, false, pathParts, isDirectory);
    return ToNS(MakePathFromParts(pathParts));
}

static bool SZIsCorrectArchiveMutationName(NSString* name) {
    const UString value = ToU(name);
    const UString lastPart = value.Ptr((unsigned)(value.ReverseFind_PathSepar() + 1));
    return !lastPart.IsEqualTo(".") && !lastPart.IsEqualTo("..");
}

static BOOL SZValidateArchiveMutationName(NSString* name, NSError** error) {
    if (SZIsCorrectArchiveMutationName(name)) {
        return YES;
    }
    if (error) {
        *error = SZMakeError(E_INVALIDARG,
            SZLocalizedString(@"app.archive.error.invalidItemName"));
    }
    return NO;
}

// MARK: - Open / Close

- (BOOL)openAtPath:(NSString*)path error:(NSError**)error {
    return [self openAtPath:path
                   openType:nil
                   password:nil
                    session:nil
                      error:error];
}

- (BOOL)openAtPath:(NSString*)path
          progress:(id<SZProgressDelegate>)progress
             error:(NSError**)error {
    return [self openAtPath:path password:nil progress:progress error:error];
}

- (BOOL)openAtPath:(NSString*)path
           session:(SZOperationSession*)session
             error:(NSError**)error {
    return [self openAtPath:path openType:nil session:session error:error];
}

- (BOOL)openAtPath:(NSString*)path
          openType:(NSString*)openType
           session:(SZOperationSession*)session
             error:(NSError**)error {
    return [self openAtPath:path
                   openType:openType
                   password:nil
                    session:session
                      error:error];
}

- (BOOL)openAtPath:(NSString*)path
          password:(NSString*)password
             error:(NSError**)error {
    return [self openAtPath:path password:password session:nil error:error];
}

- (BOOL)openAtPath:(NSString*)path
          password:(NSString*)password
          progress:(id<SZProgressDelegate>)progress
             error:(NSError**)error {
    return [self openAtPath:path
                   password:password
                    session:SZMakeDefaultOperationSession(progress)
                      error:error];
}

- (BOOL)openAtPath:(NSString*)path
          password:(NSString*)password
           session:(SZOperationSession*)session
             error:(NSError**)error {
    return [self openAtPath:path
                   openType:nil
                   password:password
                    session:session
                      error:error];
}

- (BOOL)openAtPath:(NSString*)path
          openType:(NSString*)openType
          password:(NSString*)password
           session:(SZOperationSession*)session
             error:(NSError**)error {
    SZArchiveOperationGuard operationGuard(self);

    CCodecs* codecs = SZGetCodecs();
    if (!codecs) {
        if (error)
            *error = SZMakeError(SZArchiveErrorCodeFailedToInitCodecs,
                SZLocalizedString(@"app.archive.error.failedToInitCodecs"));
        return NO;
    }
    [self close];
    [self clearCachedPassword];
    _archivePath = [path copy];

    CObjectVector<COpenType> types;
    if (openType.length > 0 && !ParseOpenTypes(*codecs, ToU(openType), types)) {
        if (error) {
            *error = SZMakeError(
                SZArchiveErrorCodeUnsupportedFormat,
                [NSString
                    stringWithFormat:SZLocalizedString(@"app.archive.error.invalidOpenType"), openType]);
        }
        return NO;
    }
    CIntVector excludedFormats;
    CObjectVector<CProperty> props;

    COpenOptions options;
    options.codecs = codecs;
    options.types = &types;
    options.excludedFormats = &excludedFormats;
    options.props = &props;
    options.stdInMode = false;
    options.stream = NULL;
    options.filePath = ToU(path);

    SZOperationSession* resolvedSession = session ?: SZMakeDefaultOperationSession(nil);
    SZOpenCallbackUI callbackUI;
    callbackUI.Session = resolvedSession;
    callbackUI.ArchivePath = ToU(path ?: @"");
    if (password) {
        callbackUI.PasswordIsDefined = true;
        callbackUI.Password = ToU(password);
    }

    HRESULT res = _arcLink->Open3(options, &callbackUI);
    if (res != S_OK) {
        if (error) {
            *error = SZOpenArchiveErrorFromResult(res, _arcLink->NonOpen_ErrorInfo,
                callbackUI, _archivePath);
        }
        return NO;
    }

    if (callbackUI.PasswordIsDefined) {
        [self storeCachedPassword:callbackUI.Password defined:true];
    }
    _openType = [openType copy];
    _isOpen = YES;
    return YES;
}

- (void)close {
    SZArchiveOperationGuard operationGuard(self);

    if (_isOpen)
        _arcLink->Close();
    _isOpen = NO;
    _openType = nil;
    [self clearCachedPassword];
}

// MARK: - Properties

- (NSString*)formatName {
    SZArchiveOperationGuard operationGuard(self);

    if (!_isOpen)
        return nil;
    const CArc& arc = _arcLink->Arcs.Back();
    CCodecs* c = SZGetCodecs();
    if (!c || arc.FormatIndex < 0)
        return nil;
    return ToNS(c->Formats[arc.FormatIndex].Name);
}

- (BOOL)canWrite {
    SZArchiveOperationGuard operationGuard(self);

    if (!_isOpen)
        return NO;
    const CArc& arc = _arcLink->Arcs.Back();
    CCodecs* c = SZGetCodecs();
    if (!c || arc.FormatIndex < 0)
        return NO;
    return c->Formats[arc.FormatIndex].UpdateEnabled;
}

- (NSArray<SZArchiveEntryProperty*>*)entryProperties {
    SZArchiveOperationGuard operationGuard(self);

    if (!_isOpen)
        return @[];
    IInArchive* archive = _arcLink->GetArchive();
    if (!archive)
        return @[];

    return SZCopyEntryProperties(archive);
}

- (NSArray<NSString*>*)entryPropertyKeys {
    NSMutableArray<NSString*>* keys = [NSMutableArray array];
    for (SZArchiveEntryProperty* property in self.entryProperties) {
        [keys addObject:property.key];
    }
    return keys;
}

- (uint64_t)archivePhysicalSize {
    SZArchiveOperationGuard operationGuard(self);

    if (!_isOpen)
        return 0;
    IInArchive* archive = _arcLink->GetArchive();
    if (!archive)
        return 0;

    NWindows::NCOM::CPropVariant value;
    if (archive->GetArchiveProperty(kpidPhySize, &value) != S_OK)
        return 0;

    if (value.vt == VT_UI8)
        return (uint64_t)value.uhVal.QuadPart;
    if (value.vt == VT_UI4)
        return (uint64_t)value.ulVal;
    if (value.vt == VT_I8)
        return value.hVal.QuadPart < 0 ? 0 : (uint64_t)value.hVal.QuadPart;
    if (value.vt == VT_I4)
        return value.lVal < 0 ? 0 : (uint64_t)value.lVal;
    return 0;
}

- (BOOL)isSolidArchive {
    SZArchiveOperationGuard operationGuard(self);

    if (!_isOpen)
        return NO;
    IInArchive* archive = _arcLink->GetArchive();
    if (!archive)
        return NO;

    NWindows::NCOM::CPropVariant value;
    if (archive->GetArchiveProperty(kpidSolid, &value) != S_OK)
        return NO;

    if (value.vt == VT_BOOL)
        return value.boolVal != VARIANT_FALSE;
    if (value.vt == VT_UI4)
        return value.ulVal != 0;
    if (value.vt == VT_I4)
        return value.lVal != 0;
    return NO;
}

- (NSUInteger)entryCount {
    SZArchiveOperationGuard operationGuard(self);

    if (!_isOpen)
        return 0;
    IInArchive* archive = _arcLink->GetArchive();
    if (!archive)
        return 0;
    UInt32 n = 0;
    archive->GetNumberOfItems(&n);
    return n;
}

- (NSArray<SZArchiveEntry*>*)entries {
    return [self entriesWithSession:nil error:nil] ?: @[];
}

- (NSArray<SZArchiveEntry*>*)entriesWithSession:(SZOperationSession*)session
                                          error:(NSError**)error {
    SZArchiveOperationGuard operationGuard(self);

    if (!_isOpen)
        return @[];
    IInArchive* archive = _arcLink->GetArchive();
    if (!archive)
        return @[];
    const CArc& arc = _arcLink->Arcs.Back();
    UInt32 n = 0;
    archive->GetNumberOfItems(&n);
    NSArray<SZArchiveEntryProperty*>* properties = SZCopyEntryProperties(archive);
    NSMutableArray* arr = [NSMutableArray arrayWithCapacity:n];
    for (UInt32 i = 0; i < n; i++) {
        if (session && [session shouldCancel]) {
            if (error)
                *error = SZMakeError(SZArchiveErrorCodeUserCancelled,
                    SZLocalizedString(@"app.archive.error.cancelled"));
            return nil;
        }

        SZArchiveEntry* e = [SZArchiveEntry new];
        e.index = i;
        CReadArcItem item;
        const bool hasReadItem = (arc.GetItem(i, item) == S_OK);
        if (hasReadItem) {
            e.path = ToNS(item.Path);
            NSMutableArray<NSString*>* pathParts =
                [NSMutableArray arrayWithCapacity:item.PathParts.Size()];
            for (unsigned j = 0; j < item.PathParts.Size(); j++) {
                [pathParts addObject:ToNS(item.PathParts[j])];
            }
            e.pathParts = pathParts;
        } else {
            UString itemPath;
            if (arc.GetItem_Path(i, itemPath) == S_OK && !itemPath.IsEmpty())
                e.path = ToNS(itemPath);
            else
                e.path = ItemStr(archive, i, kpidPath) ?: @"";
            e.pathParts = @[];
        }
        e.size = ItemU64(archive, i, kpidSize);
        e.packedSize = ItemU64(archive, i, kpidPackSize);
        e.crc = (uint32_t)ItemU64(archive, i, kpidCRC);
        e.isDirectory = hasReadItem ? item.IsDir : ItemBool(archive, i, kpidIsDir);
        e.isEncrypted = ItemBool(archive, i, kpidEncrypted);
        e.isAnti = ItemBool(archive, i, kpidIsAnti);
        e.method = ItemStr(archive, i, kpidMethod);
        e.attributes = (uint32_t)ItemU64(archive, i, kpidAttrib);
        e.position = ItemU64(archive, i, kpidPosition);
        e.block = ItemU64(archive, i, kpidBlock);
        e.modifiedDate = ItemDate(archive, i, kpidMTime);
        e.createdDate = ItemDate(archive, i, kpidCTime);
        e.accessedDate = ItemDate(archive, i, kpidATime);
        e.comment = ItemStr(archive, i, kpidComment);
        NSMutableDictionary<NSString*, NSString*>* propertyValues = [NSMutableDictionary dictionaryWithCapacity:properties.count];
        for (SZArchiveEntryProperty* property in properties) {
            NSString* value = SZPropertyDisplayString(archive, i, (PROPID)property.propID);
            if (value)
                propertyValues[property.key] = value;
        }
        e.propertyValues = propertyValues;
        [arr addObject:e];
    }
    return arr;
}

// MARK: - Extract helpers

static NExtract::NOverwriteMode::EEnum MapOverwriteMode(SZOverwriteMode m) {
    switch (m) {
    case SZOverwriteModeOverwrite:
        return NExtract::NOverwriteMode::kOverwrite;
    case SZOverwriteModeSkip:
        return NExtract::NOverwriteMode::kSkip;
    case SZOverwriteModeRename:
        return NExtract::NOverwriteMode::kRename;
    case SZOverwriteModeRenameExisting:
        return NExtract::NOverwriteMode::kRenameExisting;
    case SZOverwriteModeAsk:
    default:
        return NExtract::NOverwriteMode::kAsk;
    }
}

static NExtract::NPathMode::EEnum MapPathMode(SZPathMode m) {
    switch (m) {
    case SZPathModeCurrentPaths:
        return NExtract::NPathMode::kCurPaths;
    case SZPathModeNoPaths:
        return NExtract::NPathMode::kNoPaths;
    case SZPathModeAbsolutePaths:
        return NExtract::NPathMode::kAbsPaths;
    case SZPathModeFullPaths:
    default:
        return NExtract::NPathMode::kFullPaths;
    }
}

static UStringVector BuildRemovePathParts(NSString* pathPrefixToStrip) {
    UStringVector pathParts;
    if (!pathPrefixToStrip || pathPrefixToStrip.length == 0) {
        return pathParts;
    }

    UString path = ToU(pathPrefixToStrip);
    while (!path.IsEmpty()) {
        const wchar_t tail = path.Back();
        if (tail != L'/' && tail != L'\\') {
            break;
        }
        path.DeleteBack();
    }

    if (!path.IsEmpty()) {
        SplitPathToParts(path, pathParts);
    }
    return pathParts;
}

static constexpr unsigned kSZExtractSymLinksDangerousLevelAllowChains = 10;

static CExtractNtOptions SZExtractNtOptionsForSettings(SZExtractionSettings* settings) {
    CExtractNtOptions ntOptions;
    if (settings && settings.preserveNtSecurityInfo) {
        ntOptions.NtSecurity.Def = true;
        ntOptions.NtSecurity.Val = true;
    }

    // Level 10 is the lowest level that allows symlink chains while keeping
    // 7-Zip's lexical checks for paths that escape the extraction root, which
    // are bypassed only by level 20. macOS versioned frameworks rely on chains
    // such as Framework -> Versions/Current/Framework and Current -> A.
    ntOptions.SymLinks_DangerousLevel = kSZExtractSymLinksDangerousLevelAllowChains;
    return ntOptions;
}

static BOOL CheckExtractResult(SZFolderExtractCallback* fae, HRESULT r,
    NSError** error) {
    if (r == E_ABORT) {
        if (error)
            *error = SZMakeError(SZArchiveErrorCodeUserCancelled,
                SZLocalizedString(@"app.archive.error.cancelled"));
        return NO;
    }
    if (fae->PasswordWasWrong) {
        if (error)
            *error = SZMakeDetailedError(SZArchiveErrorCodeWrongPassword, SZLocalizedString(@"error.wrongPasswordGeneric"),
                fae->LastErrorMessage.IsEmpty() ? nil : ToNS(fae->LastErrorMessage));
        return NO;
    }
    if (r == S_OK && fae->NumErrors > 0) {
        NSString* title = [NSString stringWithFormat:@"%@ %u",
            SZLocalizedString(@"progress.errors"), fae->NumErrors];
        NSString* failureReason = fae->LastErrorMessage.IsEmpty() ? nil : ToNS(fae->LastErrorMessage);
        if (error)
            *error = SZMakeDetailedError(SZArchiveErrorCodePartialFailure, title,
                failureReason);
        return NO;
    }
    if (r != S_OK) {
        if (error)
            *error = SZMakeError(SZArchiveErrorCodeExtractionFailed,
                SZLocalizedString(@"app.archive.error.extractionFailed"));
        return NO;
    }
    return YES;
}

static BOOL EnsureExtractionDirectoryExists(NSString* dest, NSError** error) {
    if (NWindows::NFile::NDir::CreateComplexDir(us2fs(ToU(dest)))) {
        return YES;
    }

    if (error) {
        const DWORD lastError = GetLastError_noZero_HRESULT();
        NSString* failureReason = ToNS(NWindows::NError::MyFormatMessage(lastError));
        *error = SZMakeDetailedError(SZArchiveErrorCodeExtractionFailed,
            SZLocalizedString(@"create.errorFolder"),
            failureReason);
    }

    return NO;
}

static BOOL SZResolveConfiguredWorkDirForPath(NSString* path, FString& workDir, NSError** error) {
    workDir.Empty();
    NWorkDir::CInfo workDirInfo;
    workDirInfo.Load();
    if (workDirInfo.Mode == NWorkDir::NMode::kCurrent) {
        return YES;
    }

    FString fullPath;
    if (!NWindows::NFile::NDir::MyGetFullPathName(us2fs(ToU(path)), fullPath)) {
        if (error) {
            const HRESULT result = GetLastError_noZero_HRESULT();
            NSString* failureReason = ToNS(NWindows::NError::MyFormatMessage(result));
            *error = SZMakeDetailedError(result, SZLocalizedString(@"create.errorFolder"), failureReason);
        }
        return NO;
    }

    FString fileName;
    workDir = GetWorkDir(workDirInfo, fullPath, fileName);
    if (NWindows::NFile::NDir::CreateComplexDir(workDir)) {
        return YES;
    }

    if (error) {
        const HRESULT result = GetLastError_noZero_HRESULT();
        NSString* failureReason = ToNS(NWindows::NError::MyFormatMessage(result));
        *error = SZMakeDetailedError(result, SZLocalizedString(@"create.errorFolder"), failureReason);
    }
    return NO;
}

static HRESULT SZExtractAndFinalize(IInArchive* archive,
    const UInt32* indices,
    UInt32 numItems,
    Int32 testMode,
    IArchiveExtractCallback* archiveExtractCallback,
    CArchiveExtractCallback* extractCallbackImpl) {
    CArchiveExtractCallback_Closer extractCallbackCloser(extractCallbackImpl);
    HRESULT result = archive->Extract(indices, numItems, testMode, archiveExtractCallback);
    const HRESULT closeResult = extractCallbackCloser.Close();
    if (result == S_OK) {
        result = closeResult;
    }
    return result;
}

// MARK: - Extract

- (BOOL)extractToPath:(NSString*)dest
             settings:(SZExtractionSettings*)s
             progress:(id<SZProgressDelegate>)p
                error:(NSError**)error {
    return [self extractToPath:dest
                      settings:s
                       session:SZMakeDefaultOperationSession(p)
                         error:error];
}

- (BOOL)extractToPath:(NSString*)dest
             settings:(SZExtractionSettings*)s
              session:(SZOperationSession*)session
                error:(NSError**)error {
    SZArchiveOperationGuard operationGuard(self);

    if (!_isOpen) {
        if (error)
            *error = SZMakeError(SZArchiveErrorCodeNoOpenArchive,
                SZLocalizedString(@"app.fileManager.error.noArchiveOpen"));
        return NO;
    }
    IInArchive* archive = _arcLink->GetArchive();
    const CArc& arc = _arcLink->Arcs.Back();
    if (!EnsureExtractionDirectoryExists(dest, error)) {
        return NO;
    }

    SZOperationSession* resolvedSession = session ?: SZMakeDefaultOperationSession(nil);
    SZFolderExtractCallback* faeSpec = new SZFolderExtractCallback;
    CMyComPtr<IFolderArchiveExtractCallback> faeCallback(faeSpec);
    faeSpec->Session = resolvedSession;
    faeSpec->OverwriteMode = s.overwriteMode;
    faeSpec->ArchivePath = ToU(_archivePath);
    faeSpec->TestMode = false;
    NSData* quarantineData = s.sourceArchivePathForQuarantine ? SZQuarantineDataForArchivePath(s.sourceArchivePathForQuarantine) : nil;
    [self configureExtractPasswordForCallback:faeSpec
                             explicitPassword:s.password];

    CArchiveExtractCallback* ecs = new CArchiveExtractCallback;
    CMyComPtr<IArchiveExtractCallback> ec(ecs);
    CExtractNtOptions ntOptions = SZExtractNtOptionsForSettings(s);
    UStringVector removePathParts = BuildRemovePathParts(s.pathPrefixToStrip);

    ecs->InitForMulti(false, MapPathMode(s.pathMode),
        MapOverwriteMode(s.overwriteMode),
        NExtract::NZoneIdMode::kNone, false);
    if (quarantineData.length > 0) {
        ecs->ZoneBuf.CopyFrom((const Byte*)quarantineData.bytes, quarantineData.length);
    }
    ecs->Init(ntOptions, NULL, &arc, faeCallback, false, false, us2fs(ToU(dest)),
        removePathParts, false, arc.GetEstmatedPhySize());

    HRESULT r = SZExtractAndFinalize(archive, nullptr, (UInt32)(Int32)-1, 0, ec, ecs);
    [self updateCachedPasswordFromExtractCallback:faeSpec result:r];
    return CheckExtractResult(faeSpec, r, error);
}

- (BOOL)extractEntries:(NSArray<NSNumber*>*)indices
                toPath:(NSString*)dest
              settings:(SZExtractionSettings*)s
              progress:(id<SZProgressDelegate>)p
                 error:(NSError**)error {
    return [self extractEntries:indices
                         toPath:dest
                       settings:s
                        session:SZMakeDefaultOperationSession(p)
                          error:error];
}

- (BOOL)extractEntries:(NSArray<NSNumber*>*)indices
                toPath:(NSString*)dest
              settings:(SZExtractionSettings*)s
               session:(SZOperationSession*)session
                 error:(NSError**)error {
    SZArchiveOperationGuard operationGuard(self);

    if (!_isOpen) {
        if (error)
            *error = SZMakeError(SZArchiveErrorCodeNoOpenArchive,
                SZLocalizedString(@"app.fileManager.error.noArchiveOpen"));
        return NO;
    }
    IInArchive* archive = _arcLink->GetArchive();
    const CArc& arc = _arcLink->Arcs.Back();
    if (!EnsureExtractionDirectoryExists(dest, error)) {
        return NO;
    }

    SZOperationSession* resolvedSession = session ?: SZMakeDefaultOperationSession(nil);
    SZFolderExtractCallback* faeSpec = new SZFolderExtractCallback;
    CMyComPtr<IFolderArchiveExtractCallback> faeCallback(faeSpec);
    faeSpec->Session = resolvedSession;
    faeSpec->OverwriteMode = s.overwriteMode;
    faeSpec->ArchivePath = ToU(_archivePath);
    faeSpec->TestMode = false;
    NSData* quarantineData = s.sourceArchivePathForQuarantine ? SZQuarantineDataForArchivePath(s.sourceArchivePathForQuarantine) : nil;
    [self configureExtractPasswordForCallback:faeSpec
                             explicitPassword:s.password];

    CArchiveExtractCallback* ecs = new CArchiveExtractCallback;
    CMyComPtr<IArchiveExtractCallback> ec(ecs);
    CExtractNtOptions ntOptions = SZExtractNtOptionsForSettings(s);
    UStringVector removePathParts = BuildRemovePathParts(s.pathPrefixToStrip);

    ecs->InitForMulti(false, MapPathMode(s.pathMode),
        MapOverwriteMode(s.overwriteMode),
        NExtract::NZoneIdMode::kNone, false);
    if (quarantineData.length > 0) {
        ecs->ZoneBuf.CopyFrom((const Byte*)quarantineData.bytes, quarantineData.length);
    }
    ecs->Init(ntOptions, NULL, &arc, faeCallback, false, false, us2fs(ToU(dest)),
        removePathParts, false, arc.GetEstmatedPhySize());

    std::vector<UInt32> ia;
    ia.reserve(indices.count);
    for (NSNumber* n in indices)
        ia.push_back([n unsignedIntValue]);
    // UINT32_MAX is 7-Zip's "extract all" sentinel.
    if (ia.size() >= (size_t)UINT32_MAX) {
        if (error)
            *error = SZMakeError(E_INVALIDARG,
                SZLocalizedString(@"archive.tooManyItems"));
        return NO;
    }
    HRESULT r = SZExtractAndFinalize(archive, ia.data(), (UInt32)ia.size(), 0, ec, ecs);
    [self updateCachedPasswordFromExtractCallback:faeSpec result:r];
    return CheckExtractResult(faeSpec, r, error);
}

- (BOOL)testWithProgress:(id<SZProgressDelegate>)p error:(NSError**)error {
    return [self testWithSession:SZMakeDefaultOperationSession(p) error:error];
}

- (BOOL)testWithSession:(SZOperationSession*)session error:(NSError**)error {
    SZArchiveOperationGuard operationGuard(self);

    if (!_isOpen) {
        if (error)
            *error = SZMakeError(SZArchiveErrorCodeNoOpenArchive,
                SZLocalizedString(@"app.fileManager.error.noArchiveOpen"));
        return NO;
    }
    IInArchive* archive = _arcLink->GetArchive();
    const CArc& arc = _arcLink->Arcs.Back();

    SZOperationSession* resolvedSession = session ?: SZMakeDefaultOperationSession(nil);
    SZFolderExtractCallback* faeSpec = new SZFolderExtractCallback;
    CMyComPtr<IFolderArchiveExtractCallback> faeCallback(faeSpec);
    faeSpec->Session = resolvedSession;
    faeSpec->ArchivePath = ToU(_archivePath);
    faeSpec->TestMode = true;
    [self configureExtractPasswordForCallback:faeSpec explicitPassword:nil];

    CArchiveExtractCallback* ecs = new CArchiveExtractCallback;
    CMyComPtr<IArchiveExtractCallback> ec(ecs);
    CExtractNtOptions ntOptions = SZExtractNtOptionsForSettings(nil);
    UStringVector removePathParts;

    ecs->InitForMulti(false, NExtract::NPathMode::kFullPaths,
        NExtract::NOverwriteMode::kOverwrite,
        NExtract::NZoneIdMode::kNone, false);
    ecs->Init(ntOptions, NULL, &arc, faeCallback, false, true, FString(),
        removePathParts, false, arc.GetEstmatedPhySize());

    HRESULT r = SZExtractAndFinalize(archive, nullptr, (UInt32)(Int32)-1, 1, ec, ecs);
    [self updateCachedPasswordFromExtractCallback:faeSpec result:r];
    return CheckExtractResult(faeSpec, r, error);
}

- (BOOL)createFolderNamed:(NSString*)folderName
          inArchiveSubdir:(NSString*)archiveSubdir
                  session:(SZOperationSession*)session
                    error:(NSError**)error {
    SZArchiveOperationGuard operationGuard(self);

    if (!SZValidateArchiveMutationName(folderName, error)) {
        return NO;
    }

    if (!_isOpen) {
        if (error)
            *error = SZMakeError(SZArchiveErrorCodeNoOpenArchive,
                SZLocalizedString(@"app.fileManager.error.noArchiveOpen"));
        return NO;
    }

    SZOperationSession* resolvedSession = session ?: SZMakeDefaultOperationSession(nil);
    SZAgentUpdateCallback* updateSpec = new SZAgentUpdateCallback;
    CMyComPtr<IFolderArchiveUpdateCallback> updateCallback(updateSpec);
    updateSpec->Session = resolvedSession;
    updateSpec->ArchivePath = ToU(_archivePath ?: @"");
    if (_cachedPasswordIsDefined) {
        updateSpec->PasswordIsDefined = true;
        updateSpec->Password = ToU(_cachedPassword ?: @"");
    }

    CMyComPtr<IInFolderArchive> agent;
    CAgent* agentSpec = NULL;
    CMyComPtr<IFolderFolder> folder;
    HRESULT result = SZOpenAgentFolder(_archivePath, _openType, updateSpec,
        archiveSubdir, agent, agentSpec, folder);
    if (result != S_OK) {
        if (error) {
            const CArcErrorInfo errorInfo = agentSpec ? agentSpec->_archiveLink.NonOpen_ErrorInfo
                                                      : CArcErrorInfo();
            *error = SZOpenArchiveErrorFromPasswordContext(
                result, errorInfo, updateSpec->PasswordWasAsked, _archivePath);
        }
        return NO;
    }

    CMyComPtr<IFolderOperations> folderOperations;
    folder.QueryInterface(IID_IFolderOperations, &folderOperations);
    if (!folderOperations) {
        if (error) {
            *error = SZMakeError(SZArchiveErrorCodeUnsupportedFormat,
                SZLocalizedString(@"archive.updateUnsupported"));
        }
        return NO;
    }

    result = folderOperations->CreateFolder(ToU(folderName), updateCallback);

    [self syncCachedPasswordFromUpdateCallback:updateSpec result:result];

    if ((result == S_OK || updateSpec->ArchiveWasReplaced) && ![self reopenAfterExternalMutationWithSession:resolvedSession
                                                                                                      error:error]) {
        return NO;
    }

    if (result != S_OK) {
        if (error) {
            *error = SZArchiveUpdateErrorFromResult(
                result, SZLocalizedString(@"create.errorFolder"),
                updateSpec->LastErrorMessage);
        }
        return NO;
    }

    return YES;
}

- (BOOL)renameItemAtPath:(NSString*)itemPath
         inArchiveSubdir:(NSString*)archiveSubdir
                 newName:(NSString*)newName
                 session:(SZOperationSession*)session
                   error:(NSError**)error {
    SZArchiveOperationGuard operationGuard(self);

    if (!SZValidateArchiveMutationName(newName, error)) {
        return NO;
    }

    if (!_isOpen) {
        if (error)
            *error = SZMakeError(SZArchiveErrorCodeNoOpenArchive,
                SZLocalizedString(@"app.fileManager.error.noArchiveOpen"));
        return NO;
    }

    SZOperationSession* resolvedSession = session ?: SZMakeDefaultOperationSession(nil);
    SZAgentUpdateCallback* updateSpec = new SZAgentUpdateCallback;
    CMyComPtr<IFolderArchiveUpdateCallback> updateCallback(updateSpec);
    updateSpec->Session = resolvedSession;
    updateSpec->ArchivePath = ToU(_archivePath ?: @"");
    if (_cachedPasswordIsDefined) {
        updateSpec->PasswordIsDefined = true;
        updateSpec->Password = ToU(_cachedPassword ?: @"");
    }

    CMyComPtr<IInFolderArchive> agent;
    CAgent* agentSpec = NULL;
    CMyComPtr<IFolderFolder> folder;
    HRESULT result = SZOpenAgentFolder(_archivePath, _openType, updateSpec,
        archiveSubdir, agent, agentSpec, folder);
    if (result != S_OK) {
        if (error) {
            const CArcErrorInfo errorInfo = agentSpec ? agentSpec->_archiveLink.NonOpen_ErrorInfo
                                                      : CArcErrorInfo();
            *error = SZOpenArchiveErrorFromPasswordContext(
                result, errorInfo, updateSpec->PasswordWasAsked, _archivePath);
        }
        return NO;
    }

    std::vector<UInt32> indices;
    result = SZResolveFolderItemIndices(folder, @[ itemPath ], archiveSubdir, indices);
    if (result != S_OK || indices.size() != 1) {
        if (error) {
            *error = SZArchiveUpdateErrorFromResult(
                E_INVALIDARG, SZLocalizedString(@"fileop.errorRenaming"), UString());
        }
        return NO;
    }

    CMyComPtr<IFolderOperations> folderOperations;
    folder.QueryInterface(IID_IFolderOperations, &folderOperations);
    if (!folderOperations) {
        if (error) {
            *error = SZMakeError(SZArchiveErrorCodeUnsupportedFormat,
                SZLocalizedString(@"archive.updateUnsupported"));
        }
        return NO;
    }

    result = folderOperations->Rename(indices[0], ToU(newName), updateCallback);

    [self syncCachedPasswordFromUpdateCallback:updateSpec result:result];

    if ((result == S_OK || updateSpec->ArchiveWasReplaced) && ![self reopenAfterExternalMutationWithSession:resolvedSession
                                                                                                      error:error]) {
        return NO;
    }

    if (result != S_OK) {
        if (error) {
            *error = SZArchiveUpdateErrorFromResult(result,
                SZLocalizedString(@"fileop.errorRenaming"),
                updateSpec->LastErrorMessage);
        }
        return NO;
    }

    return YES;
}

- (BOOL)deleteItemsAtPaths:(NSArray<NSString*>*)itemPaths
           inArchiveSubdir:(NSString*)archiveSubdir
                   session:(SZOperationSession*)session
                     error:(NSError**)error {
    SZArchiveOperationGuard operationGuard(self);

    if (!_isOpen) {
        if (error)
            *error = SZMakeError(SZArchiveErrorCodeNoOpenArchive,
                SZLocalizedString(@"app.fileManager.error.noArchiveOpen"));
        return NO;
    }

    SZOperationSession* resolvedSession = session ?: SZMakeDefaultOperationSession(nil);
    SZAgentUpdateCallback* updateSpec = new SZAgentUpdateCallback;
    CMyComPtr<IFolderArchiveUpdateCallback> updateCallback(updateSpec);
    updateSpec->Session = resolvedSession;
    updateSpec->ArchivePath = ToU(_archivePath ?: @"");
    if (_cachedPasswordIsDefined) {
        updateSpec->PasswordIsDefined = true;
        updateSpec->Password = ToU(_cachedPassword ?: @"");
    }

    CMyComPtr<IInFolderArchive> agent;
    CAgent* agentSpec = NULL;
    CMyComPtr<IFolderFolder> folder;
    HRESULT result = SZOpenAgentFolder(_archivePath, _openType, updateSpec,
        archiveSubdir, agent, agentSpec, folder);
    if (result != S_OK) {
        if (error) {
            const CArcErrorInfo errorInfo = agentSpec ? agentSpec->_archiveLink.NonOpen_ErrorInfo
                                                      : CArcErrorInfo();
            *error = SZOpenArchiveErrorFromPasswordContext(
                result, errorInfo, updateSpec->PasswordWasAsked, _archivePath);
        }
        return NO;
    }

    std::vector<UInt32> indices;
    result = SZResolveFolderItemIndices(folder, itemPaths, archiveSubdir, indices);
    if (result != S_OK || indices.empty()) {
        if (error) {
            *error = SZArchiveUpdateErrorFromResult(
                E_INVALIDARG, SZLocalizedString(@"delete.errorDeleting"), UString());
        }
        return NO;
    }

    CMyComPtr<IFolderOperations> folderOperations;
    folder.QueryInterface(IID_IFolderOperations, &folderOperations);
    if (!folderOperations) {
        if (error) {
            *error = SZMakeError(SZArchiveErrorCodeUnsupportedFormat,
                SZLocalizedString(@"archive.updateUnsupported"));
        }
        return NO;
    }

    if (indices.size() > (size_t)UINT32_MAX) {
        if (error) {
            *error = SZMakeError(E_INVALIDARG,
                SZLocalizedString(@"archive.tooManyItems"));
        }
        return NO;
    }
    result = folderOperations->Delete(indices.data(), (UInt32)indices.size(),
        updateCallback);

    [self syncCachedPasswordFromUpdateCallback:updateSpec result:result];

    if ((result == S_OK || updateSpec->ArchiveWasReplaced) && ![self reopenAfterExternalMutationWithSession:resolvedSession
                                                                                                      error:error]) {
        return NO;
    }

    if (result != S_OK) {
        if (error) {
            *error = SZArchiveUpdateErrorFromResult(
                result, SZLocalizedString(@"delete.errorDeleting"),
                updateSpec->LastErrorMessage);
        }
        return NO;
    }

    return YES;
}

- (BOOL)addPaths:(NSArray<NSString*>*)sourcePaths
    toArchiveSubdir:(NSString*)archiveSubdir
           moveMode:(BOOL)moveMode
            session:(SZOperationSession*)session
              error:(NSError**)error {
    SZArchiveOperationGuard operationGuard(self);

    if (!_isOpen) {
        if (error)
            *error = SZMakeError(SZArchiveErrorCodeNoOpenArchive,
                SZLocalizedString(@"app.fileManager.error.noArchiveOpen"));
        return NO;
    }

    if (sourcePaths.count == 0) {
        return YES;
    }

    NSMutableArray<NSString*>* normalizedPaths =
        [NSMutableArray arrayWithCapacity:sourcePaths.count];
    NSString* folderPrefix = nil;
    std::vector<UString> pathStorage;
    pathStorage.reserve(sourcePaths.count);

    for (NSString* sourcePath in sourcePaths) {
        NSString* standardizedPath =
            [NSURL fileURLWithPath:sourcePath].standardizedURL.path;
        NSString* parentPath = [standardizedPath stringByDeletingLastPathComponent];
        NSString* leafName = [standardizedPath lastPathComponent];
        if (leafName.length == 0) {
            if (error) {
                *error = SZMakeError(E_INVALIDARG,
                    SZLocalizedString(@"app.archive.error.invalidSourcePath"));
            }
            return NO;
        }

        if (!folderPrefix) {
            folderPrefix = parentPath;
        } else if (![folderPrefix isEqualToString:parentPath]) {
            if (error) {
                *error = SZMakeError(E_INVALIDARG,
                    SZLocalizedString(@"app.archive.error.addSourcesDifferentFolders"));
            }
            return NO;
        }

        [normalizedPaths addObject:standardizedPath];
        pathStorage.push_back(ToU(leafName));
    }

    std::vector<const wchar_t*> pathPointers;
    pathPointers.reserve(pathStorage.size());
    for (const UString& path : pathStorage) {
        pathPointers.push_back(path.Ptr());
    }

    SZOperationSession* resolvedSession = session ?: SZMakeDefaultOperationSession(nil);
    SZAgentUpdateCallback* updateSpec = new SZAgentUpdateCallback;
    CMyComPtr<IFolderArchiveUpdateCallback> updateCallback(updateSpec);
    updateSpec->Session = resolvedSession;
    updateSpec->ArchivePath = ToU(_archivePath ?: @"");
    if (_cachedPasswordIsDefined) {
        updateSpec->PasswordIsDefined = true;
        updateSpec->Password = ToU(_cachedPassword ?: @"");
    }

    CMyComPtr<IInFolderArchive> agent;
    CAgent* agentSpec = NULL;
    CMyComPtr<IFolderFolder> folder;
    HRESULT result = SZOpenAgentFolder(_archivePath, _openType, updateSpec,
        archiveSubdir, agent, agentSpec, folder);
    if (result != S_OK) {
        if (error) {
            const CArcErrorInfo errorInfo = agentSpec ? agentSpec->_archiveLink.NonOpen_ErrorInfo
                                                      : CArcErrorInfo();
            *error = SZOpenArchiveErrorFromPasswordContext(
                result, errorInfo, updateSpec->PasswordWasAsked, _archivePath);
        }
        return NO;
    }

    CMyComPtr<IFolderOperations> folderOperations;
    folder.QueryInterface(IID_IFolderOperations, &folderOperations);
    if (!folderOperations) {
        if (error) {
            *error = SZMakeError(SZArchiveErrorCodeUnsupportedFormat,
                SZLocalizedString(@"archive.updateUnsupported"));
        }
        return NO;
    }

    if (pathPointers.size() > (size_t)UINT32_MAX) {
        if (error) {
            *error = SZMakeError(E_INVALIDARG,
                SZLocalizedString(@"archive.tooManyItems"));
        }
        return NO;
    }
    result = folderOperations->CopyFrom(
        moveMode ? 1 : 0, ToU(folderPrefix ?: @"").Ptr(), pathPointers.data(),
        (UInt32)pathPointers.size(), updateCallback);

    [self syncCachedPasswordFromUpdateCallback:updateSpec result:result];

    if ((result == S_OK || updateSpec->ArchiveWasReplaced) && ![self reopenAfterExternalMutationWithSession:resolvedSession
                                                                                                      error:error]) {
        return NO;
    }

    if (result != S_OK) {
        if (error) {
            *error = SZArchiveUpdateErrorFromResult(result,
                SZLocalizedStringWithFirstPlaceholder(@"archive.cannotUpdateFile", _archivePath),
                updateSpec->LastErrorMessage);
        }
        return NO;
    }

    return YES;
}

- (BOOL)replaceItemAtPath:(NSString*)itemPath
          inArchiveSubdir:(NSString*)archiveSubdir
           withFileAtPath:(NSString*)sourceFilePath
                  session:(SZOperationSession*)session
                    error:(NSError**)error {
    SZArchiveOperationGuard operationGuard(self);

    if (!_isOpen) {
        if (error)
            *error = SZMakeError(SZArchiveErrorCodeNoOpenArchive,
                SZLocalizedString(@"app.fileManager.error.noArchiveOpen"));
        return NO;
    }

    NSString* standardizedSourcePath =
        [NSURL fileURLWithPath:sourceFilePath].standardizedURL.path;
    if (![[NSFileManager defaultManager]
            fileExistsAtPath:standardizedSourcePath]) {
        if (error) {
            *error = SZMakeError(E_INVALIDARG,
                SZLocalizedString(@"app.archive.error.missingNestedArchive"));
        }
        return NO;
    }

    SZOperationSession* resolvedSession = session ?: SZMakeDefaultOperationSession(nil);
    SZAgentUpdateCallback* updateSpec = new SZAgentUpdateCallback;
    CMyComPtr<IFolderArchiveUpdateCallback> updateCallback(updateSpec);
    updateSpec->Session = resolvedSession;
    updateSpec->ArchivePath = ToU(_archivePath ?: @"");
    if (_cachedPasswordIsDefined) {
        updateSpec->PasswordIsDefined = true;
        updateSpec->Password = ToU(_cachedPassword ?: @"");
    }

    CMyComPtr<IInFolderArchive> agent;
    CAgent* agentSpec = NULL;
    CMyComPtr<IFolderFolder> folder;
    HRESULT result = SZOpenAgentFolder(_archivePath, _openType, updateSpec,
        archiveSubdir, agent, agentSpec, folder);
    if (result != S_OK) {
        if (error) {
            const CArcErrorInfo errorInfo = agentSpec ? agentSpec->_archiveLink.NonOpen_ErrorInfo
                                                      : CArcErrorInfo();
            *error = SZOpenArchiveErrorFromPasswordContext(
                result, errorInfo, updateSpec->PasswordWasAsked, _archivePath);
        }
        return NO;
    }

    std::vector<UInt32> indices;
    result = SZResolveFolderItemIndices(folder, @[ itemPath ], archiveSubdir, indices);
    if (result != S_OK || indices.size() != 1) {
        if (error) {
            *error = SZArchiveUpdateErrorFromResult(
                E_INVALIDARG, SZLocalizedStringWithFirstPlaceholder(@"archive.cannotUpdateFile", itemPath),
                UString());
        }
        return NO;
    }

    CMyComPtr<IFolderOperations> folderOperations;
    folder.QueryInterface(IID_IFolderOperations, &folderOperations);
    if (!folderOperations) {
        if (error) {
            *error = SZMakeError(SZArchiveErrorCodeUnsupportedFormat,
                SZLocalizedString(@"archive.updateUnsupported"));
        }
        return NO;
    }

    result = folderOperations->CopyFromFile(
        indices[0], ToU(standardizedSourcePath).Ptr(), updateCallback);

    [self syncCachedPasswordFromUpdateCallback:updateSpec result:result];

    if ((result == S_OK || updateSpec->ArchiveWasReplaced) && ![self reopenAfterExternalMutationWithSession:resolvedSession
                                                                                                      error:error]) {
        return NO;
    }

    if (result != S_OK) {
        if (error) {
            *error = SZArchiveUpdateErrorFromResult(
                result, SZLocalizedStringWithFirstPlaceholder(@"archive.cannotUpdateFile", itemPath),
                updateSpec->LastErrorMessage);
        }
        return NO;
    }

    return YES;
}

static UString SZCompressionMethodSpec(SZCompressionSettings* settings) {
    if (settings.methodName.length > 0) {
        return ToU(settings.methodName);
    }

    switch (settings.method) {
    case SZCompressionMethodLZMA:
        return UString(L"LZMA");
    case SZCompressionMethodLZMA2:
        return UString(L"LZMA2");
    case SZCompressionMethodPPMd:
        return UString(L"PPMd");
    case SZCompressionMethodBZip2:
        return UString(L"BZip2");
    case SZCompressionMethodDeflate:
        return UString(L"Deflate");
    case SZCompressionMethodDeflate64:
        return UString(L"Deflate64");
    case SZCompressionMethodCopy:
        return UString(L"Copy");
    }
}

static bool SZCompressionMethodUsesOrderMode(const UString& methodSpec) {
    return methodSpec.IsEqualTo_Ascii_NoCase("PPMd");
}

static UString
SZCompressionEncryptionProperty(SZCompressionSettings* settings) {
    if (settings.password.length == 0) {
        return UString();
    }

    if (settings.format == SZArchiveFormatZip) {
        // Zip should only use an explicit encryption mode when a password is set.
        switch (settings.encryption) {
        case SZEncryptionMethodAES256:
            return UString(L"AES256");
        case SZEncryptionMethodZipCrypto:
            return UString(L"ZipCrypto");
        case SZEncryptionMethodNone:
            // Let createAtPath: reject password + no explicit zip mode.
            return UString();
        }
    }

    return UString();
}

static const NUpdateArchive::CActionSet&
SZCompressionActionSetForMode(SZCompressionUpdateMode mode) {
    switch (mode) {
    case SZCompressionUpdateModeUpdate:
        return NUpdateArchive::k_ActionSet_Update;
    case SZCompressionUpdateModeFresh:
        return NUpdateArchive::k_ActionSet_Fresh;
    case SZCompressionUpdateModeSync:
        return NUpdateArchive::k_ActionSet_Sync;
    case SZCompressionUpdateModeAdd:
    default:
        return NUpdateArchive::k_ActionSet_Add;
    }
}

static NWildcard::ECensorPathMode
SZMapCompressionPathMode(SZCompressionPathMode mode) {
    switch (mode) {
    case SZCompressionPathModeFullPaths:
        return NWildcard::k_FullPath;
    case SZCompressionPathModeAbsolutePaths:
        return NWildcard::k_AbsPath;
    case SZCompressionPathModeRelativePaths:
    default:
        return NWildcard::k_RelatPath;
    }
}

static void SZAddCompressionProperty(CObjectVector<CProperty>& properties,
    const wchar_t* name,
    const UString& value) {
    CProperty property;
    property.Name = name;
    property.Value = value;
    properties.Add(property);
}

static void SZAddCompressionPropertyUInt32(CObjectVector<CProperty>& properties,
    const wchar_t* name, UInt32 value) {
    UString text;
    text.Add_UInt32(value);
    SZAddCompressionProperty(properties, name, text);
}

static void SZAddCompressionPropertySize(CObjectVector<CProperty>& properties,
    const wchar_t* name, UInt64 value) {
    UString text;
    text.Add_UInt64(value);
    text.Add_Char('b');
    SZAddCompressionProperty(properties, name, text);
}

static void SZAddCompressionPropertyBool(CObjectVector<CProperty>& properties,
    const wchar_t* name, bool value) {
    SZAddCompressionProperty(properties, name, UString(value ? L"on" : L"off"));
}

static CBoolPair SZCompressionBoolPair(SZCompressionBoolSetting setting) {
    CBoolPair pair;
    if (setting != SZCompressionBoolSettingNotDefined) {
        pair.Def = true;
        pair.Val = (setting == SZCompressionBoolSettingOn);
    }
    return pair;
}

static bool SZLocateBundledWindowsSfxModule(FString& sfxModule) {
    NSString* path = [[NSBundle mainBundle] pathForResource:@"7z" ofType:@"sfx"];
    if (path.length == 0) {
        return false;
    }

    sfxModule = us2fs(ToU(path));
    return true;
}

static void SZSplitOptionsToStrings(const UString& src,
    UStringVector& strings) {
    SplitString(src, strings);
    FOR_VECTOR(i, strings) {
        UString& option = strings[i];
        if (option.Len() > 2 && option[0] == '-' && MyCharLower_Ascii(option[1]) == 'm') {
            option.DeleteFrontal(2);
        }
    }
}

static void SZAddCensorExclude(NWildcard::CCensor& censor, const wchar_t* path,
    bool recursive, bool wildcardMatching,
    Byte markMode) {
    NWildcard::CCensorPathProps props;
    props.Recursive = recursive;
    props.WildcardMatching = wildcardMatching;
    props.MarkMode = markMode;
    censor.AddPreItem(false, UString(path), props);
}

static void SZAddMacResourceFileExcludes(NWildcard::CCensor& censor) {
    SZAddCensorExclude(censor, L".DS_Store", true, false,
        NWildcard::kMark_StrictFile);
    SZAddCensorExclude(censor, L"._*", true, true, NWildcard::kMark_StrictFile);
    SZAddCensorExclude(censor, L"Icon\r", true, false,
        NWildcard::kMark_StrictFile);
    SZAddCensorExclude(censor, L".VolumeIcon.icns", true, false,
        NWildcard::kMark_StrictFile);
    SZAddCensorExclude(censor, L".apdisk", true, false,
        NWildcard::kMark_StrictFile);

    SZAddCensorExclude(censor, L"__MACOSX/", true, false,
        NWildcard::kMark_FileOrDir);
    SZAddCensorExclude(censor, L".Spotlight-V100/", true, false,
        NWildcard::kMark_FileOrDir);
    SZAddCensorExclude(censor, L".Trashes/", true, false,
        NWildcard::kMark_FileOrDir);
    SZAddCensorExclude(censor, L".fseventsd/", true, false,
        NWildcard::kMark_FileOrDir);
    SZAddCensorExclude(censor, L".TemporaryItems/", true, false,
        NWildcard::kMark_FileOrDir);
    SZAddCensorExclude(censor, L".DocumentRevisions-V100/", true, false,
        NWildcard::kMark_FileOrDir);
}

static bool SZHasMethodOverride(bool is7z, const UStringVector& strings) {
    FOR_VECTOR(i, strings) {
        const UString& option = strings[i];
        if (is7z) {
            const wchar_t* end = NULL;
            const UInt64 number = ConvertStringToUInt64(option, &end);
            if (number == 0 && *end == L'=') {
                return true;
            }
        } else if (option.Len() > 1 && option[0] == L'm' && option[1] == L'=') {
            return true;
        }
    }

    return false;
}

static void
SZParseAndAddCompressionProperties(CObjectVector<CProperty>& properties,
    const UStringVector& strings) {
    FOR_VECTOR(i, strings) {
        const UString& option = strings[i];
        CProperty property;
        const int separatorIndex = option.Find(L'=');
        if (separatorIndex < 0) {
            property.Name = option;
        } else {
            property.Name.SetFrom(option, (unsigned)separatorIndex);
            property.Value = option.Ptr(separatorIndex + 1);
        }
        properties.Add(property);
    }
}

static bool SZParseVolumeSizes(const UString& text,
    CRecordVector<UInt64>& values) {
    values.Clear();
    bool previousTokenWasNumber = false;

    for (unsigned index = 0; index < text.Len();) {
        wchar_t character = text[index++];
        if (character == L' ') {
            continue;
        }
        if (character == L'-') {
            return true;
        }

        if (previousTokenWasNumber) {
            previousTokenWasNumber = false;
            unsigned shiftBits = 0;
            switch (MyCharLower_Ascii(character)) {
            case 'b':
                continue;
            case 'k':
                shiftBits = 10;
                break;
            case 'm':
                shiftBits = 20;
                break;
            case 'g':
                shiftBits = 30;
                break;
            case 't':
                shiftBits = 40;
                break;
            }

            if (shiftBits != 0) {
                UInt64& value = values.Back();
                if (value >= ((UInt64)1 << (64 - shiftBits))) {
                    return false;
                }
                value <<= shiftBits;

                for (; index < text.Len(); index++) {
                    if (text[index] == L' ') {
                        break;
                    }
                }
                continue;
            }
        }

        index--;
        const wchar_t* start = text.Ptr(index);
        const wchar_t* end = NULL;
        const UInt64 value = ConvertStringToUInt64(start, &end);
        if (start == end || value == 0) {
            return false;
        }
        values.Add(value);
        previousTokenWasNumber = true;
        index += (unsigned)(end - start);
    }

    return true;
}

// MARK: - Create

+ (BOOL)createAtPath:(NSString*)archivePath
           fromPaths:(NSArray<NSString*>*)src
            settings:(SZCompressionSettings*)s
            progress:(id<SZProgressDelegate>)p
               error:(NSError**)error {
    return [self createAtPath:archivePath
                    fromPaths:src
                     settings:s
                      session:SZMakeDefaultOperationSession(p)
                        error:error];
}

+ (BOOL)createAtPath:(NSString*)archivePath
           fromPaths:(NSArray<NSString*>*)src
            settings:(SZCompressionSettings*)s
             session:(SZOperationSession*)session
               error:(NSError**)error {
    CCodecs* codecs = SZGetCodecs();
    if (!codecs) {
        if (error)
            *error = SZMakeError(-1, SZLocalizedString(@"app.archive.error.failedToInitCodecs"));
        return NO;
    }

    const int methodID = SZCompressionEstimateMethodID(s);

    NSString* formatName = SZArchiveCodecNameForCreateFormat(s.format);
    if (formatName.length == 0) {
        if (error)
            *error = SZMakeError(SZArchiveErrorCodeUnsupportedFormat,
                SZLocalizedString(@"archive.unsupportedType"));
        return NO;
    }

    // Reject zip passwords without an explicit encryption mode.
    if (s.format == SZArchiveFormatZip
        && s.password.length > 0
        && s.encryption == SZEncryptionMethodNone) {
        if (error) {
            *error = SZMakeError(E_INVALIDARG,
                SZLocalizedString(@"app.archive.error.zipPasswordRequiresEncryption"));
        }
        return NO;
    }

    CUpdateOptions options;
    options.Commands.Clear();
    CUpdateArchiveCommand command;
    command.ActionSet = SZCompressionActionSetForMode(s.updateMode);
    options.Commands.Add(command);
    options.PathMode = SZMapCompressionPathMode(s.pathMode);
    options.OpenShareForWrite = s.openSharedFiles;
    options.DeleteAfterCompressing = s.deleteAfterCompression;
    options.SfxMode = s.createSFX;

    if (s.createSFX) {
        const int sfxMethodID = methodID;
        if (s.format != SZArchiveFormat7z || !SZCompressionEstimateMethodSupportsSFX(sfxMethodID)) {
            if (error)
                *error = SZMakeError(-8,
                    SZLocalizedString(@"app.archive.error.sfxUnsupportedMethod"));
            return NO;
        }
        if (!SZLocateBundledWindowsSfxModule(options.SfxModule)) {
            if (error)
                *error = SZMakeError(
                    -1,
                    SZLocalizedString(@"app.archive.error.missingWindowsSFX"));
            return NO;
        }
    }

    options.SymLinks = SZCompressionBoolPair(s.storeSymbolicLinks);
    options.HardLinks = SZCompressionBoolPair(s.storeHardLinks);
    options.AltStreams = SZCompressionBoolPair(s.storeAlternateDataStreams);
    options.NtSecurity = SZCompressionBoolPair(s.storeFileSecurity);
    if (s.preserveSourceAccessTime != SZCompressionBoolSettingNotDefined) {
        options.PreserveATime = (s.preserveSourceAccessTime == SZCompressionBoolSettingOn);
    }
    if (s.setArchiveTimeToLatestFile != SZCompressionBoolSettingNotDefined) {
        options.SetArcMTime = (s.setArchiveTimeToLatestFile == SZCompressionBoolSettingOn);
    }

    const UString fmtName = ToU(formatName);
    int formatIndex = codecs->FindFormatForArchiveType(fmtName);
    if (formatIndex < 0) {
        NSString* ext = [[archivePath pathExtension] lowercaseString];
        formatIndex = codecs->FindFormatForExtension(ToU(ext));
    }
    if (formatIndex < 0) {
        if (error)
            *error = SZMakeError(-8, SZLocalizedString(@"archive.unsupportedType"));
        return NO;
    }
    options.MethodMode.Type.FormatIndex = formatIndex;
    options.MethodMode.Type_Defined = true;

    const CArcInfoEx& formatInfo = codecs->Formats[(unsigned)formatIndex];
    const bool is7z = formatInfo.Is_7z();
    const UString methodSpec = SZCompressionMethodSpec(s);
    const bool usesOrderMode = SZCompressionMethodUsesOrderMode(methodSpec);

    UStringVector optionStrings;
    if (s.parameters.length > 0) {
        SZSplitOptionsToStrings(ToU(s.parameters), optionStrings);
    }
    const bool methodOverride = SZHasMethodOverride(is7z, optionStrings);

    // Match upstream 7-Zip ZS GUI encoding for ZSTD fast levels.
    SZAddCompressionPropertyUInt32(
        options.MethodMode.Properties, L"x",
        SZCompressionLevelPropertyValue(methodID, s.levelValue));

    if (!methodSpec.IsEmpty() && !methodOverride) {
        SZAddCompressionProperty(options.MethodMode.Properties, is7z ? L"0" : L"m",
            methodSpec);
    }

    if (s.dictionarySize > 0) {
        const wchar_t* propertyName = usesOrderMode ? (is7z ? L"0mem" : L"mem") : (is7z ? L"0d" : L"d");
        SZAddCompressionPropertySize(options.MethodMode.Properties, propertyName,
            (UInt64)s.dictionarySize);
    }

    if (s.wordSize > 0) {
        const wchar_t* propertyName = usesOrderMode ? (is7z ? L"0o" : L"o") : (is7z ? L"0fb" : L"fb");
        SZAddCompressionPropertyUInt32(options.MethodMode.Properties, propertyName,
            s.wordSize);
    }

    const UString encryptionProperty = SZCompressionEncryptionProperty(s);
    if (!encryptionProperty.IsEmpty()) {
        SZAddCompressionProperty(options.MethodMode.Properties, L"em",
            encryptionProperty);
    }

    if (s.numThreads > 0) {
        CProperty p2;
        p2.Name = L"mt";
        wchar_t buf[16];
        swprintf(buf, 16, L"%u", (unsigned)s.numThreads);
        p2.Value = buf;
        options.MethodMode.Properties.Add(p2);
    }
    if ((s.format == SZArchiveFormat7z || s.format == SZArchiveFormatXz) && s.solidMode) {
        CProperty p2;
        p2.Name = L"s";
        p2.Value = L"on";
        options.MethodMode.Properties.Add(p2);
    }
    if (s.encryptFileNames && s.format == SZArchiveFormat7z && s.password.length > 0) {
        CProperty p2;
        p2.Name = L"he";
        p2.Value = L"on";
        options.MethodMode.Properties.Add(p2);
    }

    if (s.storeModificationTime != SZCompressionBoolSettingNotDefined) {
        SZAddCompressionPropertyBool(options.MethodMode.Properties, L"tm",
            s.storeModificationTime == SZCompressionBoolSettingOn);
    }
    if (s.storeCreationTime != SZCompressionBoolSettingNotDefined) {
        SZAddCompressionPropertyBool(options.MethodMode.Properties, L"tc",
            s.storeCreationTime == SZCompressionBoolSettingOn);
    }
    if (s.storeAccessTime != SZCompressionBoolSettingNotDefined) {
        SZAddCompressionPropertyBool(options.MethodMode.Properties, L"ta",
            s.storeAccessTime == SZCompressionBoolSettingOn);
    }
    if (s.timePrecision != SZCompressionTimePrecisionAutomatic) {
        SZAddCompressionPropertyUInt32(options.MethodMode.Properties, L"tp",
            (UInt32)s.timePrecision);
    }
    if (s.memoryUsage.length > 0) {
        SZAddCompressionProperty(options.MethodMode.Properties, L"memuse",
            ToU(s.memoryUsage));
    }

    if (optionStrings.Size() > 0) {
        SZParseAndAddCompressionProperties(options.MethodMode.Properties,
            optionStrings);
    }

    if (s.splitVolumes.length > 0) {
        if (!SZParseVolumeSizes(ToU(s.splitVolumes), options.VolumesSizes)) {
            if (error)
                *error = SZMakeError(-1,
                    SZLocalizedString(@"split.incorrectVolumeSize"));
            return NO;
        }
    } else if (s.splitVolumeSize > 0) {
        options.VolumesSizes.Add(s.splitVolumeSize);
    }

    if (s.createSFX && options.VolumesSizes.Size() > 0) {
        if (error)
            *error = SZMakeError(
                -1, SZLocalizedString(@"app.archive.error.sfxCannotSplitVolumes"));
        return NO;
    }

    NWildcard::CCensor censor;
    for (NSString* srcPath in src) {
        censor.AddPreItem_NoWildcard(ToU(srcPath));
    }
    if (s.excludeMacResourceFiles) {
        SZAddMacResourceFileExcludes(censor);
    }

    SZOperationSession* resolvedSession = session ?: SZMakeDefaultOperationSession(nil);
    SZUpdateCallbackUI callbackUI;
    callbackUI.Session = resolvedSession;
    if (s.password.length > 0) {
        callbackUI.PasswordIsDefined = true;
        callbackUI.Password = ToU(s.password);
    }

    SZOpenCallbackUI openCallbackUI;
    openCallbackUI.Session = resolvedSession;
    openCallbackUI.ArchivePath = ToU(archivePath ?: @"");
    // Reopen encrypted archives with the write password to avoid a second prompt.
    if (callbackUI.PasswordIsDefined) {
        openCallbackUI.PasswordIsDefined = true;
        openCallbackUI.Password = callbackUI.Password;
    }
    CUpdateErrorInfo errorInfo;
    CObjectVector<COpenType> types;

    if (!SZResolveConfiguredWorkDirForPath(archivePath, options.WorkingDir, error)) {
        return NO;
    }

    HRESULT r = UpdateArchive(codecs, types, ToU(archivePath), censor, options,
        errorInfo, &openCallbackUI, &callbackUI, true);

    if (r != S_OK) {
        NSString* desc;
        if (r == E_ABORT)
            desc = SZLocalizedString(@"app.archive.error.compressionCancelled");
        else if (errorInfo.Message.Len() > 0)
            desc = NSFromCString(errorInfo.Message.Ptr());
        else
            desc = [NSString
                stringWithFormat:SZLocalizedString(@"app.archive.error.compressionFailedFormat"), (unsigned)r];
        if (error)
            *error = SZMakeError(r, desc);
        return NO;
    }
    return YES;
}

// MARK: - Formats

+ (NSArray<SZFormatInfo*>*)supportedFormats {
    CCodecs* codecs = SZGetCodecs();
    if (!codecs)
        return @[];
    NSMutableArray* arr = [NSMutableArray array];
    for (unsigned i = 0; i < codecs->Formats.Size(); i++) {
        const CArcInfoEx& ai = codecs->Formats[i];
        SZFormatInfo* info = [SZFormatInfo new];
        info.name = ToNS(ai.Name);
        NSMutableArray* exts = [NSMutableArray array];
        for (unsigned j = 0; j < ai.Exts.Size(); j++)
            [exts addObject:ToNS(ai.Exts[j].Ext)];
        info.extensions = exts;
        info.canWrite = ai.UpdateEnabled;
        info.supportsSymbolicLinks = ai.Flags_SymLinks();
        info.supportsHardLinks = ai.Flags_HardLinks();
        info.supportsAlternateDataStreams = ai.Flags_AltStreams();
        info.supportsFileSecurity = ai.Flags_NtSecurity();
        info.supportsModificationTime = ai.Flags_MTime();
        info.supportsCreationTime = ai.Flags_CTime();
        info.supportsAccessTime = ai.Flags_ATime();
        info.defaultsModificationTime = ai.Flags_MTime_Default();
        info.defaultsCreationTime = ai.Flags_CTime_Default();
        info.defaultsAccessTime = ai.Flags_ATime_Default();
        info.keepsName = ai.Flags_KeepName();

        UInt32 defaultTimePrecision = ai.Get_DefaultTimePrec();
        if (ai.Is_GZip()) {
            defaultTimePrecision = (UInt32)SZCompressionTimePrecisionUnix;
        }

        UInt32 supportedTimePrecisionMask = ai.Get_TimePrecFlags();
        if (defaultTimePrecision < 32) {
            supportedTimePrecisionMask |= ((UInt32)1 << defaultTimePrecision);
        }
        info.supportedTimePrecisionMask = supportedTimePrecisionMask;
        if (defaultTimePrecision <= (UInt32)SZCompressionTimePrecisionLinux) {
            info.defaultTimePrecision = (SZCompressionTimePrecision)defaultTimePrecision;
        } else {
            info.defaultTimePrecision = SZCompressionTimePrecisionAutomatic;
        }

        [arr addObject:info];
    }
    return arr;
}

// MARK: - Hash

+ (NSDictionary<NSString*, NSString*>*)calculateHashForPath:(NSString*)path
                                                      error:
                                                          (NSError**)error {
    return [self calculateHashForPath:path session:nil error:error];
}

+ (NSDictionary<NSString*, NSString*>*)
    calculateHashForPath:(NSString*)path
                 session:(SZOperationSession*)session
                   error:(NSError**)error {
    CCodecs* codecs = SZGetCodecs();
    if (!codecs) {
        if (error)
            *error = SZMakeError(-1, SZLocalizedString(@"app.archive.error.failedToInitCodecs"));
        return nil;
    }

    CHashOptions options;
    options.Methods.Add(UString(L"CRC32"));
    options.Methods.Add(UString(L"CRC64"));
    options.Methods.Add(UString(L"XXH64"));
    options.Methods.Add(UString(L"MD5"));
    options.Methods.Add(UString(L"SHA1"));
    options.Methods.Add(UString(L"SHA256"));
    options.Methods.Add(UString(L"SHA384"));
    options.Methods.Add(UString(L"SHA512"));
    options.Methods.Add(UString(L"SHA3-256"));
    options.Methods.Add(UString(L"BLAKE2sp"));

    NWildcard::CCensor censor;
    NWildcard::CCensorPathProps props;
    props.Recursive = false;
    censor.AddItem(NWildcard::k_AbsPath, true, ToU(path), props);

    class HashCB : public IHashCallbackUI {
    public:
        __weak SZOperationSession* session;
        NSMutableDictionary* results;
        UString failureDescription;
        UString failureReason;
        HRESULT failureResult;
        UInt64 totalSize;

        HashCB(SZOperationSession* resolvedSession)
            : session(resolvedSession)
            , results([NSMutableDictionary dictionary])
            , failureResult(S_OK)
            , totalSize(0) {
        }

        bool HasFailure() const { return failureResult != S_OK; }

        HRESULT StartScanning() override { return CheckBreak(); }
        HRESULT FinishScanning(const CDirItemsStat&) override {
            return CheckBreak();
        }
        HRESULT SetNumFiles(UInt64) override { return CheckBreak(); }
        HRESULT SetTotal(UInt64 size) override {
            totalSize = size;
            if (session && size > 0) {
                [session reportProgressFraction:0.0];
                [session reportBytesCompleted:0 total:size];
            }
            return CheckBreak();
        }
        HRESULT SetCompleted(const UInt64* completed) override {
            if (session && completed && totalSize > 0) {
                UInt64 value = *completed;
                if (value > totalSize) {
                    value = totalSize;
                }
                [session reportProgressFraction:(double)value / (double)totalSize];
                [session reportBytesCompleted:value total:totalSize];
            }
            return CheckBreak();
        }
        HRESULT CheckBreak() override {
            return (session && [session shouldCancel]) ? E_ABORT : S_OK;
        }
        HRESULT BeforeFirstFile(const CHashBundle&) override {
            return CheckBreak();
        }
        HRESULT GetStream(const wchar_t* name, bool) override {
            if (session && name) {
                [session reportCurrentFileName:ToNS(UString(name))];
            }
            return CheckBreak();
        }
        HRESULT OpenFileError(const FString& path, DWORD errorCode) override {
            RecordFailure(SZLocalizedString(@"app.archive.error.hashOpenFailed"), path, errorCode);
            return S_FALSE;
        }
        HRESULT SetOperationResult(UInt64, const CHashBundle& hb, bool) override {
            for (unsigned i = 0; i < hb.Hashers.Size(); i++) {
                const CHasherState& h = hb.Hashers[i];
                char hex[256];
                HashHexToString(hex, h.Digests[0], h.DigestSize);
                results[NSFromCString(h.Name.Ptr())] = NSFromCString(hex);
            }
            return S_OK;
        }
        HRESULT AfterLastFile(CHashBundle&) override {
            if (session && totalSize > 0) {
                [session reportProgressFraction:1.0];
                [session reportBytesCompleted:totalSize total:totalSize];
            }
            return CheckBreak();
        }
        HRESULT ScanError(const FString& path, DWORD errorCode) override {
            RecordFailure(SZLocalizedString(@"app.archive.error.hashScanFailed"), path, errorCode);
            return S_FALSE;
        }
        HRESULT ScanProgress(const CDirItemsStat&, const FString& path,
            bool) override {
            if (session && !path.IsEmpty()) {
                [session reportCurrentFileName:ToNS(fs2us(path))];
            }
            return CheckBreak();
        }

    private:
        void RecordFailure(NSString* description, const FString& path,
            DWORD errorCode) {
            if (HasFailure()) {
                return;
            }

            failureDescription = ToU(description ?: @"");
            failureReason = fs2us(path);

            const UString systemMessage = NWindows::NError::MyFormatMessage(errorCode);
            if (!failureReason.IsEmpty() && !systemMessage.IsEmpty()) {
                failureReason += L"\n\n";
            }
            failureReason += systemMessage;
            failureResult = (errorCode == 0) ? E_FAIL : HRESULT_FROM_WIN32(errorCode);
        }
    };

    HashCB cb(session);
    AString errorInfo;
    HRESULT r = HashCalc(EXTERNAL_CODECS_LOC_VARS censor, options, errorInfo, &cb);

    if (cb.HasFailure()) {
        if (error) {
            NSString* description = cb.failureDescription.IsEmpty()
                ? SZLocalizedString(@"app.archive.error.hashFailed")
                : ToNS(cb.failureDescription);
            NSString* reason = cb.failureReason.IsEmpty() ? nil : ToNS(cb.failureReason);
            *error = SZMakeDetailedError(cb.failureResult, description, reason);
        }
        return nil;
    }

    if (r != S_OK) {
        NSString* reason = errorInfo.IsEmpty()
            ? nil
            : NSFromCString(errorInfo.Ptr());
        if (error)
            *error = SZMakeDetailedError(r,
                SZLocalizedString(@"app.archive.error.hashFailed"), reason);
        return nil;
    }

    return cb.results;
}

@end
