77 lines
3.1 KiB
Go
77 lines
3.1 KiB
Go
package bookfile
|
|
|
|
import (
|
|
"archive/zip"
|
|
"fmt"
|
|
"hash/crc32"
|
|
"io"
|
|
)
|
|
|
|
// container.go: the OCF container — the zip layout every EPUB shares, independent of what the package
|
|
// inside says. It is the one place the layout is spelled, for the shipping writer (epub.go) and for the
|
|
// ingest test fixture (internal/chunk/chunktest) alike.
|
|
|
|
// mimetypeBody is the fixed content of the OCF `mimetype` entry.
|
|
const mimetypeBody = "application/epub+zip"
|
|
|
|
// Container is an OCF container under construction. Entries land in the order they are added, with
|
|
// ZERO timestamps (archive/zip leaves the MS-DOS date fields at zero and writes no extended-timestamp
|
|
// extra field when FileHeader.Modified is unset), so the bytes depend on the entries alone — never on the
|
|
// clock or on a filesystem — which is what makes a build reproducible across processes.
|
|
type Container struct {
|
|
zw *zip.Writer
|
|
}
|
|
|
|
// NewContainer starts an OCF container on w whose package document (the OPF) will be added at
|
|
// rootfile — a path inside the zip such as "OEBPS/content.opf". It writes the two entries OCF fixes:
|
|
// `mimetype` FIRST and STORED (uncompressed, so a reader can identify the file by its first bytes),
|
|
// then META-INF/container.xml pointing at rootfile. The caller adds rootfile itself with Add.
|
|
func NewContainer(w io.Writer, rootfile string) (*Container, error) {
|
|
zw := zip.NewWriter(w)
|
|
// CreateRaw with the CRC and sizes given up front: the local header then carries them itself and
|
|
// no data descriptor follows the entry. OCF's letter is met either way, but a reader that
|
|
// identifies the file by its first bytes wants the plain form, and the 20 bytes cost nothing.
|
|
body := []byte(mimetypeBody)
|
|
mw, err := zw.CreateRaw(&zip.FileHeader{
|
|
Name: "mimetype", Method: zip.Store, CRC32: crc32.ChecksumIEEE(body),
|
|
CompressedSize64: uint64(len(body)), UncompressedSize64: uint64(len(body)),
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("bookfile: container mimetype: %w", err)
|
|
}
|
|
if _, err := mw.Write(body); err != nil {
|
|
return nil, fmt.Errorf("bookfile: container mimetype: %w", err)
|
|
}
|
|
c := &Container{zw: zw}
|
|
container := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
|
|
<rootfiles>
|
|
<rootfile full-path="` + xmlText(rootfile) + `" media-type="application/oebps-package+xml"/>
|
|
</rootfiles>
|
|
</container>
|
|
`
|
|
if err := c.Add("META-INF/container.xml", []byte(container)); err != nil {
|
|
return nil, err
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// Add writes one entry (deflated, zero timestamp) at name — a slash-separated path inside the zip.
|
|
func (c *Container) Add(name string, data []byte) error {
|
|
w, err := c.zw.CreateHeader(&zip.FileHeader{Name: name, Method: zip.Deflate})
|
|
if err != nil {
|
|
return fmt.Errorf("bookfile: container entry %s: %w", name, err)
|
|
}
|
|
if _, err := w.Write(data); err != nil {
|
|
return fmt.Errorf("bookfile: container entry %s: %w", name, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Close finishes the zip (central directory). The container is complete only after it returns nil.
|
|
func (c *Container) Close() error {
|
|
if err := c.zw.Close(); err != nil {
|
|
return fmt.Errorf("bookfile: close container: %w", err)
|
|
}
|
|
return nil
|
|
}
|