ProjectBundle.h header
#include <ew/app/ProjectBundle.h>
Namespace ew::app
Functions
std::optional< QMap< QString, QString > > ew::app::persistence::bundleBinaryFingerprints(const QByteArray &bundleBytes)
The binary (media asset) entries of the bundle bundleBytes as a forward-slash relative path -> hex SHA-1 map – the bundle counterpart of readBinaryFingerprints, so external asset changes inside a replaced bundle are detected. Partitioned against bundleTextSnapshot by the same extension rule. Nullopt on an invalid or not-fully-readable archive, never a partial map – see bundleTextSnapshot for why a short map is dangerous rather than merely incomplete.
QString ew::app::persistence::bundleSha256(const QByteArray &bytes)
Hex SHA-256 of bytes – the content fingerprint the working-copy metadata records for a bundle file, and the external-change detector compares against.
std::optional< QMap< QString, QString > > ew::app::persistence::bundleTextSnapshot(const QByteArray &bundleBytes)
The text entries of the bundle bundleBytes as a forward-slash relative path -> content map – the bundle counterpart of readTextSnapshot (the same extension partition, with CRLF normalized to LF exactly as QIODevice::Text does on read), so a bundle file replaced on disk by a sync client can feed the same three-way reconcile a folder project uses, without extracting to a temp folder. Unsafe, directory-marker, and dot-segment entries are skipped.
Returns nullopt if the archive is invalid OR any listed entry will not read – never a partial map. This is the whole contract: a partially-materialized cloud file (OneDrive mid-sync) lists its entries but cannot produce their data, and a SHORT snapshot is indistinguishable from a snapshot in which those files were deleted. Callers feed this to a three-way reconcile, which would then delete the user's live objects to match the gap.
ew::core::foundation::Result< void > ew::app::persistence::extractProjectBundle(const QString &bundlePath, const QString &destDir)
Extracts the bundle at bundlePath into destDir, which must not yet exist or must be an empty directory – the wipe of a stale working copy belongs to the caller that provably owns the folder, so the one destructive step never lives here. The bundle is fully validated BEFORE anything is written: it must be a well-formed ZIP containing project.json at its root, and every file entry's name must be a safe relative path – an absolute path, a drive or UNC prefix, or a .. segment (zip-slip) rejects the whole bundle as hostile. Directory-marker entries (a trailing '/') and entries with a dot-prefixed segment are skipped: our packer never writes them, but archives zipped by hand may carry them, and "a plain ZIP of the project folder renamed to .ewriter" deliberately opens.
bool ew::app::persistence::isProjectBundlePath(const QString &path)
True when path names a single-file project bundle – either extension, case-insensitive.
This is the routing predicate for every seam that accepts "a project path": a bundle path goes through the bundle machinery, anything else is treated as a folder-of-text project directory. (See docs/SINGLE_FILE_PROJECTS.md.)
Accepting both extensions here is what makes legacy files work everywhere at once, because every such seam routes through this one function rather than testing the extension itself. The bundle FORMAT is identical either way – a ZIP with project.json at its root – so nothing downstream needs to know which extension it arrived under.
QString ew::app::persistence::legacyProjectBundleExtension()
The extension bundles used before the product was renamed, including the leading dot.
Read forever. This is not deprecated and has no sunset: users have .ewriter files on disk, in backups, and registered with the shell, and every one of them must keep opening. The only thing that changed is which extension NEW bundles are given.
ew::core::foundation::Result< QByteArray > ew::app::persistence::packProjectBundle(const QString &projectDir)
Packs the folder-of-text project at projectDir into .ewriter bundle bytes: a ZIP of every file under the folder whose relative path has no dot-prefixed segment (VCS metadata, the derived index, and hidden files never enter a bundle), keyed by forward-slash relative path. Entries are sorted and makeZip uses a fixed timestamp, so packing the same folder twice yields byte-identical archives – the determinism the working-copy crash-recovery byte-compare relies on. Errors when the folder is missing, is not a project (no project.json at its root), or any file cannot be read: the bundle is the user's save artifact, so a partial pack would be silent data loss and is refused outright.
QString ew::app::persistence::projectBundleExtension()
The extension for single-file project bundles this build creates, including the leading dot.
New bundles get this one. Existing files keep whatever extension they already have – see isProjectBundlePath.
ew::core::foundation::Result< void > ew::app::persistence::writeProjectBundle(const QString &projectDir, const QString &bundlePath)
Packs projectDir (see packProjectBundle) and writes the archive to bundlePath atomically: QSaveFile commits (renames) only after a full write, so an interrupted save (crash, full disk) leaves the previous bundle intact rather than truncated. Parent directories of bundlePath are created as needed.