All formats

System.Formats.Tar

LanguageC# / .NET 7+
LicenseMIT (part of .NET)
Repositorygithub.com/dotnet/runtime
DocumentationSystem.Formats.Tar on Microsoft Learn
Depends onnothing (built into .NET 7 and later)
Read / writeyes / yes (V7, ustar, pax and GNU formats)

System.Formats.Tar is the TAR implementation built into .NET since version 7. TarReader walks the entries of a stream in order; TarWriter appends entries; TarFile has one-call helpers to extract or create an archive from a folder. It reads pax extended headers and GNU long names, so names of any length work.

Like every TAR reader it’s sequential: to list pages it reads every header. With copyData: false (the default), the entry’s DataStream is only valid until you move to the next entry, so copy what you need as you go.

Installation#

Nothing to install on .NET 7+: using System.Formats.Tar;.

Basic usage: list pages in natural order#

Comic.IsPage and Comic.NaturalCompare are the helpers from the System.IO.Compression CBZ page: an image-extension filter that skips __MACOSX and dotfiles (including ._ AppleDouble files), and a natural, case-insensitive compare.

C#
using System.Formats.Tar;

static List<string> ListPages(string path)
{
    var pages = new List<string>();
    using FileStream fs = File.OpenRead(path);
    using var reader = new TarReader(fs);
    TarEntry? entry;
    while ((entry = reader.GetNextEntry()) != null)
    {
        if (entry.EntryType is TarEntryType.RegularFile or TarEntryType.V7RegularFile
            && Comic.IsPage(entry.Name))
            pages.Add(entry.Name);
    }
    pages.Sort(Comic.NaturalCompare);
    return pages;
}

foreach (var (name, i) in ListPages(args[0]).Select((n, i) => (n, i)))
    Console.WriteLine($"{i + 1,3} {name}");

How to read all pages into memory#

C#
static Dictionary<string, byte[]> ReadPages(string path)
{
    var pages = new Dictionary<string, byte[]>();
    using FileStream fs = File.OpenRead(path);
    using var reader = new TarReader(fs);
    TarEntry? entry;
    while ((entry = reader.GetNextEntry()) != null)
    {
        if (entry.DataStream == null || !Comic.IsPage(entry.Name)) continue;
        using var ms = new MemoryStream((int)entry.Length);
        entry.DataStream.CopyTo(ms);
        pages[entry.Name] = ms.ToArray();
    }
    return pages;
}

How to extract the cover#

The cover is the first page in natural order. ExtractToFile writes the current entry to disk:

C#
string? cover = ListPages("comic.cbt").FirstOrDefault();
if (cover != null)
{
    using FileStream fs = File.OpenRead("comic.cbt");
    using var reader = new TarReader(fs);
    TarEntry? entry;
    while ((entry = reader.GetNextEntry()) != null)
    {
        if (entry.Name == cover)
        {
            entry.ExtractToFile("cover" + Path.GetExtension(cover).ToLowerInvariant(), overwrite: true);
            break;
        }
    }
}

How to read ComicInfo.xml#

C#
using System.Formats.Tar;
using System.Xml.Linq;

using FileStream fs = File.OpenRead("comic.cbt");
using var reader = new TarReader(fs);
TarEntry? entry;
while ((entry = reader.GetNextEntry()) != null)
{
    if (!entry.Name.Equals("ComicInfo.xml", StringComparison.OrdinalIgnoreCase) || entry.DataStream == null)
        continue;
    XElement ci = XDocument.Load(entry.DataStream).Root!;
    Console.WriteLine($"{ci.Element("Series")?.Value} #{ci.Element("Number")?.Value}: {ci.Element("Title")?.Value}");
    // <Page Image="0" Type="FrontCover"/>: Image is the index in sorted page order
    var coverPage = ci.Element("Pages")?.Elements("Page")
        .FirstOrDefault(p => ((string?)p.Attribute("Type") ?? "").Contains("FrontCover"));
    if (coverPage != null) Console.WriteLine($"cover index {(int)coverPage.Attribute("Image")!}");
    break;
}

How to create a CBT from a folder of images#

TarWriter.WriteEntry(fileName, entryName) adds a file from disk. Use the pax format (the default) and no compression:

C#
using System.Formats.Tar;

static void MakeCbt(string dir, string output)
{
    var files = Directory.GetFiles(dir).Where(Comic.IsPage)
        .OrderBy(f => Path.GetFileName(f), Comparer<string>.Create(Comic.NaturalCompare))
        .ToList();

    using FileStream fs = File.Create(output);
    using var writer = new TarWriter(fs, TarEntryFormat.Pax, leaveOpen: false);
    for (int i = 0; i < files.Count; i++)
        writer.WriteEntry(files[i], $"{i + 1:D3}{Path.GetExtension(files[i]).ToLowerInvariant()}");
    string info = Path.Combine(dir, "ComicInfo.xml");
    if (File.Exists(info))
        writer.WriteEntry(info, "ComicInfo.xml");
}

When the folder already has zero-padded names, TarFile.CreateFromDirectory(dir, "comic.cbt", includeBaseDirectory: false) does it in one call.

Tips#