Skip to content

Commit acb3333

Browse files
committed
Improve documentation
1 parent 608ee79 commit acb3333

5 files changed

Lines changed: 182 additions & 24 deletions

File tree

‎CLAUDE.md‎

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -232,16 +232,27 @@ its opener and the magic numbers `Sniff` answers to, and `BookContainers` only s
232232
which containers are in the build.
233233

234234
Everything downstream reads the two registries, so neither addition edits a third
235-
file. In particular: the Settings form's context-menu checkboxes and the file-dialog
236-
filter are both built from `Extensions`, the log's name for a format comes from
237-
`FormatId.DisplayName`, and `BookFormats.FromExtension` and `BookContainers.Sniff`
238-
answer from what is registered rather than from a `switch`. **If a change of yours
239-
needs a list of formats or extensions written out a second time, that is the bug.**
235+
file. In particular: the Settings form's context-menu checkboxes, the file-dialog
236+
filter and the About box are all built from `Extensions`, the log's name for a format
237+
comes from `FormatId.DisplayName`, and `BookFormats.FromExtension` and
238+
`BookContainers.Sniff` answer from what is registered rather than from a `switch`.
239+
**If a change of yours needs a list of formats or extensions written out a second
240+
time, that is the bug.** Nothing outside `BookContainers`' six `Register` lines names
241+
a container type at all, and there is no `switch` on `ContainerKind` anywhere.
242+
243+
`ExtensionPointTests` is that promise made executable: it adds a container and a
244+
format from the *test* assembly, registers them, and opens a file end to end through
245+
`Book.Load` — no Core file edited. It is what fails if the inventory gets hardcoded
246+
again.
240247

241248
The exceptions, all deliberate:
242249

243-
- **`FormatId` and `ContainerKind`** are enums, so a new one is a line in each plus a
244-
line in `DisplayName`.
250+
- **`ContainerKind`** is an enum, so a new container is also a line there. That, the
251+
file, and the `Register` call are the complete list for a container nothing needs
252+
to *read as a book*.
253+
- **A container holds books only once a format claims it.** For a comic archive that
254+
is one row in `CbzFormat.Flavours`, plus a `FormatId` and its `DisplayName` line;
255+
for anything else, a new `IBookFormat`.
245256
- **`FormatCapabilities`** must be stated per format — see **Formats**.
246257
- **`.fb2.zip`** is the one compound extension. `Fb2Format` declares it, the file
247258
dialog offers it, and `ShellRegistration` drops it, because

‎README.md‎

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ There are already excellent ebook-management tools, but none quite matched the w
2020

2121
* **Edit files directly from Explorer.** No library import or database required.
2222
* **Batch-edit an entire collection.** Select multiple files, open a folder, or paste values across dozens of books at once.
23-
* **Support the formats I actually use.** EPUB, CBZ, CBT, CBR, FB2, MOBI, AZW, and AZW3.
23+
* **Support the formats I actually use.** EPUB, CBZ, CBT, CBR, CB7, FB2, MOBI, AZW, and AZW3.
2424
* **Write metadata correctly.** The editor does not just insert values into an OPF or `ComicInfo.xml`; it validates and normalizes the surrounding metadata where possible.
2525
* **Avoid pretending formats support things they do not.** Fields that cannot be stored safely are disabled instead of being silently discarded or written into undocumented locations.
2626

@@ -32,22 +32,22 @@ There are already excellent ebook-management tools, but none quite matched the w
3232
| CBZ | ✅ | ✅ | ZIP | `ComicInfo.xml` |
3333
| CBT | ✅ | ✅ | TAR | `ComicInfo.xml` |
3434
| CBR | ✅ | ⚙️ | RAR | `ComicInfo.xml` |
35+
| CB7 | ✅ | ⚙️ | 7z | `ComicInfo.xml` |
3536
| FB2 | ✅ | ✅ | Plain XML | `<description>` |
3637
| FB2.ZIP | ✅ | ✅ | ZIP | `<description>` |
3738
| MOBI / PRC | ✅ | ✅ | PalmDB | EXTH records |
3839
| AZW / AZW3 | ✅ | ✅ | PalmDB | EXTH records |
3940

40-
**⚙️ CBR reads on its own; saving one needs WinRAR installed.**.
41-
So EBookMetaEditor reads CBR files out of the box, and to save one it uses the `Rar.exe` that comes with WinRAR:
42-
it looks for your WinRAR installation in the registry, then for `rar.exe` on your `PATH`.
41+
**⚙️ CBR and CB7 read on their own; saving one needs the matching archiver installed.**
42+
EBookMetaEditor reads both out of the box. Writing them needs a compressor it is not allowed to ship, so to save a CBR it runs the `Rar.exe` that comes with WinRAR, and to save a CB7 the `7z.exe` that comes with 7-Zip. It looks for the installation in the registry, then for the program on your `PATH`. If neither turns up, the save is refused and your file is left exactly as it was — nothing is half-written, and no other format is affected.
4343

44-
**Not supported:** CB7, PDF, KFX, AZW4, LIT, PDB, RB, DjVu, and audiobooks.
44+
**Not supported:** PDF, KFX, AZW4, LIT, PDB, RB, DjVu, and audiobooks.
4545

4646
## Editable metadata
4747

4848
Not every format can store the same metadata. EBookMetaEditor disables fields that a particular file cannot preserve rather than accepting a value that would later be lost.
4949

50-
| Field | EPUB | CBZ / CBT / CBR | FB2 | MOBI / AZW3 |
50+
| Field | EPUB | CBZ / CBT / CBR / CB7 | FB2 | MOBI / AZW3 |
5151
| ------------------------ | ------------ | ------------ | ------------ | ------------ |
5252
| Title | read + write | read + write | read + write | read + write |
5353
| Sort title | read + write | — | — | — |
@@ -59,7 +59,6 @@ Not every format can store the same metadata. EBookMetaEditor disables fields th
5959
| Description | read + write | read + write | read + write | read + write |
6060
| Publisher | read + write | read + write | read + write | read + write |
6161
| Publication date | read + write | read + write | read + write | read + write |
62-
| Modification date | read + write | — | — | — |
6362
| Language | read + write | read + write | read + write | read + write |
6463
| Subjects / tags | read + write | read + write | read + write | read + write |
6564
| Identifiers (ISBN, etc.) | read + write | — | read | read |
@@ -107,9 +106,9 @@ Unmodified files are not rewritten — they are not even opened for writing. Eve
107106

108107
Each row reports its own result, so one bad file does not stop the rest of the batch. For example:
109108

110-
* A `.cbz` that is actually a 7z archive is reported as unsupported, by name.
111-
* A `.cbz` that is actually a RAR opens as a CBR, and the mismatch is still reported.
112-
* A CBR row reads and edits like any other; on save it either goes through the `Rar.exe` found on the machine or reports that the save failed.
109+
* A `.cbz` that is actually a RAR opens as a CBR, and one that is actually a 7z opens as a CB7. Either way the mismatch is still reported.
110+
* A `.cbz` that is actually a PDF is reported as unsupported, by name.
111+
* A CBR or CB7 row reads and edits like any other; on save it either goes through the archiver found on the machine or reports that the save failed.
113112
* A DRM-protected AZW is reported as non-editable.
114113
* A file that fails to save fails independently without aborting the remaining files.
115114

‎src/EBookMeta.Core/IBookFormat.cs‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using EBookMeta.Model;
1+
using EBookMeta.Model;
22

33
namespace EBookMeta;
44

@@ -76,12 +76,15 @@ public enum FormatId
7676
Cbz,
7777

7878
/// <summary>
79-
/// Comic archive, RAR. <c>ComicInfo.xml</c>, read but never written — see
80-
/// <c>RarContainer</c>.
79+
/// Comic archive, RAR. <c>ComicInfo.xml</c>, written only where the machine has
80+
/// an archiver — see <c>RarContainer</c>.
8181
/// </summary>
8282
Cbr,
8383

84-
/// <summary>Comic archive, 7z.</summary>
84+
/// <summary>
85+
/// Comic archive, 7z. <c>ComicInfo.xml</c>, written only where the machine has
86+
/// an archiver — see <c>SevenZipContainer</c>.
87+
/// </summary>
8588
Cb7,
8689

8790
/// <summary>Comic archive, TAR.</summary>

‎src/EBookMeta.Core/IContainer.cs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,10 @@ public enum ContainerKind
6363
/// <summary>ZIP.</summary>
6464
Zip,
6565

66-
/// <summary>RAR, versions 4 and 5. Readable; never rebuilt.</summary>
66+
/// <summary>RAR, versions 4 and 5. Rebuilt only through an archiver on the machine.</summary>
6767
Rar,
6868

69-
/// <summary>7z.</summary>
69+
/// <summary>7z. Rebuilt only through an archiver on the machine.</summary>
7070
SevenZip,
7171

7272
/// <summary>TAR.</summary>
@@ -212,7 +212,7 @@ public sealed record ContainerEntry
212212
/// <summary>
213213
/// Whether an entry name is absolute, or walks out of the archive with <c>..</c>.
214214
/// Hard invariant 4, and the one predicate for it, so the read path and
215-
/// <c>RarContainer.Stage</c> cannot disagree about what "escapes" means.
215+
/// <c>ExternalArchiver.Stage</c> cannot disagree about what "escapes" means.
216216
/// </summary>
217217
public static bool EscapesArchive(string? name)
218218
{

‎src/EBookMeta.Core/README.md‎

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,146 @@ broken, and writing it back without disturbing anything else. Zero UI dependenci
55
— that is enforced by the `GuardCoreHasNoUiDependencies` target in the csproj, not
66
by review.
77

8+
## How it fits together
9+
10+
Two axes, deliberately the same shape. A **container** knows how to get bytes out of a
11+
file and how to write a new one; a **format** knows what the metadata document inside
12+
those bytes means. Neither has heard of the other, and `Book` is the only thing
13+
holding one of each.
14+
15+
```
16+
┌──────────────────────────┐
17+
│ Book │
18+
│ │
19+
│ Load(path) Save() │
20+
└─────────────┬────────────┘
21+
│ asks both registries and names
22+
│ no implementation of either
23+
┌─────────────────┴─────────────────┐
24+
│ │
25+
seam 1 · the metadata document seam 2 · the bytes it sits in
26+
│ │
27+
┌─────────────▼────────────┐ ┌─────────────▼────────────┐
28+
│ BookFormats │ │ BookContainers │
29+
│ │ │ │
30+
│ Register For │ │ Register For │
31+
│ TryOpen FromExtension │ │ Open Sniff │
32+
└─────────────┬────────────┘ └─────────────┬────────────┘
33+
│ the registry of │
34+
┌─────────────▼────────────┐ ┌─────────────▼────────────┐
35+
│ IBookFormat │ │ IContainer │
36+
│ │ │ │
37+
│ Id Extensions │ │ Entries (order kept) │
38+
│ Capabilities │ │ IsWritable │
39+
│ TryOpen(BookSource) │ │ OpenRead(entry) │
40+
│ Read(container) │ │ Rebuild(pending, path) │
41+
│ Write(container, path) │ │ │
42+
├──────────────────────────┤ ├──────────────────────────┤
43+
│ EpubFormat .epub │ │ ZipContainer ZIP │
44+
│ CbzFormat .cbz .cbt │ │ TarContainer TAR │
45+
│ .cbr .cb7 │ │ RarContainer RAR * │
46+
│ Fb2Format .fb2 │ │ SevenZipContainer 7z * │
47+
│ .fb2.zip │ │ PalmDbContainer PalmDB │
48+
│ MobiFormat .mobi .prc │ │ RawContainer none │
49+
│ .azw .azw3 │ └─────────────┬────────────┘
50+
└──────────────────────────┘ │ * cannot compress itself
51+
┌─────────────▼────────────┐
52+
│ ExternalArchiver │
53+
│ │
54+
│ finds and runs the │
55+
│ rar.exe / 7z.exe that │
56+
│ is on the machine │
57+
└──────────────────────────┘
58+
```
59+
60+
Most of the design falls out of that picture:
61+
62+
- **The two axes are independent.** `ZipContainer` has never heard of EPUB and
63+
`EpubFormat` never opens a file. That is why CBZ, CBT, CBR and CB7 are one format
64+
class living in four containers, and why adding a container is a file and a
65+
`Register` line.
66+
- **The registries are the only way across.** Nothing above them ever says
67+
`new ZipContainer()` — `Book` asks for a `ContainerKind` and gets an `IContainer`.
68+
- **There is no second path.** `BatchSession` is a list of `Book`s; saving a row of
69+
the grid calls `Book.Save`, the same one the single-file window calls. Five hundred
70+
files are five hundred independent saves, not a batch write.
71+
72+
### Opening a file
73+
74+
```
75+
Book.Load("comic.cbz")
76+
│
77+
├─▶ BookFormats.TryOpen(path)
78+
│ │
79+
│ ├─▶ BookSource.Open(path) the file is opened once; 8 KB is read
80+
│ │ └─▶ BookContainers.Sniff(head)
81+
│ │ "PK\x03\x04" at offset 0 ─▶ ContainerKind.Zip
82+
│ │
83+
│ ├─▶ offer that one BookSource to every registered format
84+
│ │ EpubFormat ─▶ null no mimetype entry
85+
│ │ CbzFormat ─▶ Strong holds ComicInfo.xml
86+
│ │ Fb2Format ─▶ null no .fb2 entry
87+
│ │ MobiFormat ─▶ null not a PalmDB
88+
│ │ the strongest MatchConfidence wins, never registration order
89+
│ │
90+
│ └─▶ source.Container ─▶ BookContainers.Open(path, Zip) ─▶ ZipContainer
91+
│ opened on first use and handed to the winner still open
92+
│
93+
├─▶ CheckEntryNames(container) GEN-E003 for a name that escapes
94+
│
95+
└─▶ CbzFormat.Read(container)
96+
└─▶ container.OpenRead("ComicInfo.xml") ─▶ BookMetadata
97+
```
98+
99+
**Almost nothing runs here**, which is most of why a cold launch stays under 400 ms.
100+
A read parses the metadata document and stops: no entry is decompressed to decide what
101+
a file is, and no page is touched.
102+
103+
### Saving a file
104+
105+
```
106+
Book.Save()
107+
│
108+
└─▶ AtomicFileWriter.Write(path, …) the only sanctioned way a file is replaced
109+
│
110+
├ 1 BookContainers.Open(path, Zip)
111+
│ reopened inside the callback, so the read handle is shut before the
112+
│ swap pulls the file out from under it
113+
│
114+
├ 2 CbzFormat.Write(container, metadata, "comic.cbz.tmp")
115+
│ │ every correction happens here, never on open:
116+
│ │ CBZ-W010 no ComicInfo.xml ─▶ one is created
117+
│ │ CBZ-E011 it sits in a folder ─▶ moved to the root
118+
│ │ CBZ-E020 PageCount is wrong ─▶ recounted from the images
119+
│ │
120+
│ └─▶ container.Rebuild(PendingEntry[], "comic.cbz.tmp")
121+
│ the metadata document is replaced; every other entry is
122+
│ copied through byte for byte
123+
│
124+
└ 3 File.Replace(comic.cbz.tmp ─▶ comic.cbz, backup comic.cbz.bak)
125+
```
126+
127+
`Read` parses and stops; **`Write` is where every repair lives**. A repair therefore
128+
cannot reach the disk unless the user saves, which is what makes "the file on disk is
129+
what you last saved" true by construction rather than by care.
130+
131+
### When the container cannot compress itself
132+
133+
```
134+
RarContainer.Rebuild(entries, "comic.cbr.tmp") SevenZipContainer is identical
135+
│
136+
├─ no archiver on this machine
137+
│ └─▶ CBR-F002 / CB7-F002 — refused, and the user's file is left untouched
138+
│
139+
└─▶ ExternalArchiver
140+
├─ Stage() writes every entry under comic.cbr.tmp.stage\
141+
│ the one place in Core that extracts to disk, and therefore
142+
│ where ".." and duplicate names are refused outright
143+
├─ writes __entries.lst — UTF-16, one relative name per line
144+
└─ runs rar.exe / 7z.exe ─▶ comic.cbr.tmp
145+
every way that can fail becomes the same BookIoException
146+
```
147+
8148
## Read these six files, in this order
9149

10150
| # | File | Why |
@@ -40,6 +180,11 @@ compress itself — RAR, 7z — supplies an `ExternalArchiver` with the name of
40180
program to find, the registry keys that record where it installed, and its command
41181
line, and gets the staging, the list file and the one failure answer for free.
42182

183+
In full, and this is the whole list: the file, a `ContainerKind` member, the
184+
`Register` line. Nothing else in Core names a container, and nothing switches on
185+
`ContainerKind` — `ExtensionPointTests` proves it by adding a container and a format
186+
from the test assembly and opening a file through both.
187+
43188
**A comic archive is smaller still.** `CbzFormat.Flavours` pairs a `FormatId` with a
44189
`ContainerKind` and an extension; a new row plus the container is the whole change,
45190
and `BookFormats` does not need editing.

0 commit comments

Comments
 (0)