Core .NET library for working with .mdz archives.
Formerly published as
mdzip-core. The package was renamed toMDZip.Corestarting with version 1.4.0 to match theMDZip.*NuGet ID scheme;mdzip-coreis deprecated but remains listed for back-compat.
MDZip.Core provides APIs to:
- create
.mdzarchives from folders or explicit file mappings - extract archives safely
- list archive contents
- read
manifest.json - resolve the archive entry point
- validate archives against path and manifest rules
- Package ID:
MDZip.Core - Target frameworks:
.NET 8,.NET 10 - Output assembly:
MDZip.Core.dll - Main namespace:
MDZip.Core
Install the package from NuGet.org:
dotnet add package MDZip.Coreusing MDZip.Core;
using MDZip.Core.Models;
// Create from a directory
MdzArchive.Create(
outputPath: "book.mdz",
sourceDirectory: "content",
manifest: new Manifest
{
Title = "My Document",
EntryPoint = "index.md"
}); // spec.version, mode and timestamps are filled in
// Validate
var result = MdzArchive.Validate("book.mdz");
Console.WriteLine(result.IsValid);
// List entries
var files = MdzArchive.List("book.mdz");
// Add or replace a file in-place
MdzArchive.AddFile("book.mdz", "assets/notes.txt", "local-notes.txt");
// Remove a file in-place
MdzArchive.RemoveFile("book.mdz", "assets/old-image.png");
// Extract
MdzArchive.Extract("book.mdz", "out");All methods are static on MdzArchive and take the archive's file path first.
Read:
ResolveEntryPoint(archivePath)→ the main document's path, ornull;ResolveEntryPointStrict(archivePath)throws with the reason insteadResolveMode(archivePath)→"document"or"project"ReadManifest(archivePath)ReadText(archivePath, entryPath),ReadBytes(...),ReadBase64(...),ReadDataUri(archivePath, entryPath, fallbackMime = null)List(archivePath),ListDetailed(archivePath),ListSpecialFiles(archivePath),BuildPathTree(...)HasEntry(archivePath, entryPath),FindEntry(...),FindCaseOnlyMatch(...)ResolveLink(archivePath, document, link, host = null, archiveHostPath = "archive.mdz", use = "navigate")(see below)OpenWorkspace(archivePath, options = null): documents and assets as one model, for editorsInferMimeType(path),ClassifyAssetKind(path),IsPreviewableAsset(path)
Create:
Create(outputPath, sourceDirectory, manifest = null)CreateFromFiles(outputPath, files, manifest = null):filesare(ArchivePath, LocalPath)pairsBuildArchive(outputPath, files, rootName, options),BuildWorkspace(outputPath, workspace, options = null)CreateManifest(metadata = null)
Change an existing archive (entries you don't touch and unknown manifest fields are kept):
UpdateFiles(archivePath, writes, removals = null, manifest = null, dropSpecialFiles = false):writesareArchiveWriteSpec(ArchivePath, LocalPath)AddFile(archivePath, entryPath, localFilePath),RemoveFile(archivePath, entryPath),RemoveFiles(archivePath, entryPaths)UpdateManifest(archivePath, metadata, options = null),SplitManifestMetadata(manifest)FindOrphanedAssets(archivePath, options = null): images no document uses
Check and extract:
Validate(archivePath),ValidateManifest(manifestJson),GetValidationStatus(result)Extract(archivePath, destinationDirectory)
The spec's rules are public too: MdzSpec (paths, entry point, manifest checks, ErrorCode) and MdzLinks.ResolveLink.
Image paths are relative to the document that uses them. Resolve each one with ResolveLink, using use: "embed":
var entry = MdzArchive.ResolveEntryPointStrict("notes.mdz");
var markdown = MdzArchive.ReadText("notes.mdz", entry);
// For each image src in the document (Markdown and HTML <img> alike):
var target = MdzArchive.ResolveLink("notes.mdz", entry, src, use: "embed");
if (target.Kind == "internal")
src = MdzArchive.ReadDataUri("notes.mdz", target.Entry!);Percent-encoding in src (my%20photo.png) is decoded for you. Kind is one of:
Kind |
Meaning | Fields |
|---|---|---|
internal |
An entry in this archive | Entry, Fragment |
url |
A URL with a scheme, such as https:, data: or mailto: |
|
host |
A file outside the archive (needs a host that knows where the archive is) |
Path, Fragment |
archive |
An entry in another .mdz (needs a host) |
Archive, Entry, Fragment |
unresolved |
Not found, invalid (for example /abs.png), or outside the trust boundary |
Reason |
use is "navigate" (the default) for links the user follows, or "embed" for resources the document loads by itself, such as images. Embeds that leave the archive are unresolved unless the host allows outside resources (spec §9.6).
Version 2.0.0 implements MDZip spec 1.2.0 and runs its shared fixtures (tests/mdzip-core.Tests/Fixtures, synced with mdzip-spec/tools/sync-fixtures.mjs --ref v1.2.1). Things to know when upgrading from 1.x:
- Paths are case-sensitive after Unicode NFC normalization (§5.4.1):
Index.mdno longer findsindex.md.MdzArchive.FindCaseOnlyMatchsuggests the near match after a failed lookup. - Every read checks the container first: encrypted entries (
ERR_ZIP_ENCRYPTED), compression methods other than stored/DEFLATE (ERR_ZIP_INVALID), two entries equal after NFC (ERR_DUPLICATE_PATH) and a file that is also a directory (ERR_PATH_CONFLICT) throw. - ZIP-bomb limits (§13.1): opening rejects an archive with more than
MdzArchive.MaxEntries(100,000) entries, an entry overMaxEntryUncompressedBytes(1 GiB), more thanMaxTotalUncompressedBytes(4 GiB) in total, or an entry over 1 MiB compressed more thanMaxCompressionRatio(1000:1), withERR_LIMIT_EXCEEDED. The limits are fixed. - Reads are verified: every read, extraction and update copy stops at the entry's declared size and checks its CRC-32. Data that runs past the declared size is
ERR_LIMIT_EXCEEDED; data that is short, fails the checksum or won't decompress isERR_ZIP_INVALID. Extraction deletes a file it couldn't write completely. - Symbolic link entries are skipped and never followed: they're not listed, can't be read or extracted, are listed by
ListSpecialFilesand reported byValidate. Mutations keep them unlessdropSpecialFiles: true. - Entry point (§5.5):
manifest.entryPoint(invalid →ERR_ENTRYPOINT_INVALID, missing file →ERR_ENTRYPOINT_MISSING, never a fallback); else rootindex.md; else the single root Markdown file once support files (README, AGENTS, LICENSE, CHANGELOG, CONTRIBUTING) are set aside.ResolveEntryPointreturns null when there is none;ResolveEntryPointStrictsays why. - Manifests (§6.4): an invalid critical field (
spec,spec.version,mode,entryPoint) is an error; an invalid descriptive field is ignored — left out ofReadManifestand reported as aValidatewarning.nullcounts as a wrong type. - Producers always write
manifest.jsonwithspec.version1.2.0,entryPointandmode. Edits (AddFile,RemoveFile,UpdateFiles,UpdateManifest) keep every existing manifest field, nested unknown ones included, add a manifest to an archive without one, and throwERR_ENTRYPOINT_CHANGEDif a change would move the entry point. - Line endings: only Markdown files and
manifest.jsonare normalized to LF when written (§12.2). - Errors are
InvalidOperationExceptions whose message starts with the spec code;MdzSpec.ErrorCode(ex)reads it. - The rules are public too:
MdzSpec(paths, entry point, manifest checks) andMdzLinks.ResolveLink(links inside, out of and into archives, §9.3–9.6).
Build locally:
dotnet restore src/mdzip-core/mdzip-core.csproj
dotnet build src/mdzip-core/mdzip-core.csproj --configuration ReleasePack locally:
dotnet pack src/mdzip-core/mdzip-core.csproj --configuration Release -o out