System.Formats.Tar
| Language | C# / .NET 7+ |
|---|---|
| License | MIT (part of .NET) |
| Repository | github.com/dotnet/runtime |
| Documentation | System.Formats.Tar on Microsoft Learn |
| Depends on | nothing (built into .NET 7 and later) |
| Read / write | yes / 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.
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#
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:
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#
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:
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#
TarFile.ExtractToDirectory(path, dest, overwriteFiles: false)extracts everything and rejects entries that would land outsidedest.- Pass
copyData: truetoGetNextEntrywhen you keepTarEntryobjects after moving on, or when the input stream isn’t seekable. - For CBZ, CBR and CB7 in the same code, use SharpCompress.