Skip to content

Commit

Permalink
Add gzip-wrapping functions to CompressedResource
Browse files Browse the repository at this point in the history
  • Loading branch information
chocolatkey committed Sep 21, 2024
1 parent 26f64b0 commit 232b8c3
Show file tree
Hide file tree
Showing 10 changed files with 175 additions and 11 deletions.
19 changes: 14 additions & 5 deletions cmd/rwp/cmd/serve/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,11 +256,20 @@ func (s *Server) getAsset(w http.ResponseWriter, r *http.Request) {
}

cres, ok := res.(fetcher.CompressedResource)
if ok && cres.CompressedAs(archive.CompressionMethodDeflate) && start == 0 && end == 0 && supportsDeflate(r) {
// Stream the asset in compressed format
w.Header().Set("content-encoding", "deflate")
w.Header().Set("content-length", strconv.FormatInt(cres.CompressedLength(), 10))
_, err = cres.StreamCompressed(w)
if ok && cres.CompressedAs(archive.CompressionMethodDeflate) && start == 0 && end == 0 {
// Stream the asset in compressed format if supported by the user agent
if supportsEncoding(r, "deflate") {
w.Header().Set("content-encoding", "deflate")
w.Header().Set("content-length", strconv.FormatInt(cres.CompressedLength(), 10))
_, err = cres.StreamCompressed(w)
} else if supportsEncoding(r, "gzip") && l <= archive.GzipMaxLength {
w.Header().Set("content-encoding", "gzip")
w.Header().Set("content-length", strconv.FormatInt(cres.CompressedLength()+archive.GzipWrapperLength, 10))
_, err = cres.StreamCompressedGzip(w)
} else {
// Fall back to normal streaming
_, rerr = res.Stream(w, start, end)
}
} else {
// Stream the asset
_, rerr = res.Stream(w, start, end)
Expand Down
4 changes: 2 additions & 2 deletions cmd/rwp/cmd/serve/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,15 @@ func conformsToAsMimetype(conformsTo manifest.Profiles) string {
return mime
}

func supportsDeflate(r *http.Request) bool {
func supportsEncoding(r *http.Request, encoding string) bool {
vv := r.Header.Values("Accept-Encoding")
for _, v := range vv {
for _, sv := range strings.Split(v, ",") {
coding := parseCoding(sv)
if coding == "" {
continue
}
if coding == "deflate" {
if coding == encoding {
return true
}
}
Expand Down
8 changes: 6 additions & 2 deletions pkg/archive/archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,12 @@ type Entry interface {
CompressedAs(compressionMethod CompressionMethod) bool // Whether the entry is compressed using the given method.
Read(start int64, end int64) ([]byte, error) // Reads the whole content of this entry, or a portion when [start] or [end] are specified.
Stream(w io.Writer, start int64, end int64) (int64, error) // Streams the whole content of this entry to a writer, or a portion when [start] or [end] are specified.
StreamCompressed(w io.Writer) (int64, error) // Streams the compressed content of this entry to a writer.
ReadCompressed() ([]byte, error) // Reads the compressed content of this entry.

StreamCompressed(w io.Writer) (int64, error) // Streams the compressed content of this entry to a writer.
StreamCompressedGzip(w io.Writer) (int64, error) // Streams the compressed content of this entry to a writer in a GZIP container.
ReadCompressed() ([]byte, error) // Reads the compressed content of this entry.
ReadCompressedGzip() ([]byte, error) // Reads the compressed content of this entry inside a GZIP container.

}

// Represents an immutable archive.
Expand Down
8 changes: 8 additions & 0 deletions pkg/archive/archive_exploded.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,18 @@ func (e explodedArchiveEntry) StreamCompressed(w io.Writer) (int64, error) {
return -1, errors.New("entry is not compressed")
}

func (e explodedArchiveEntry) StreamCompressedGzip(w io.Writer) (int64, error) {
return -1, errors.New("entry is not compressed")
}

func (e explodedArchiveEntry) ReadCompressed() ([]byte, error) {
return nil, errors.New("entry is not compressed")
}

func (e explodedArchiveEntry) ReadCompressedGzip() ([]byte, error) {
return nil, errors.New("entry is not compressed")
}

// An archive exploded on the file system as a directory.
type explodedArchive struct {
directory string // Directory, already cleaned!
Expand Down
75 changes: 74 additions & 1 deletion pkg/archive/archive_zip.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import (
"archive/zip"
"bytes"
"compress/flate"
"errors"
"encoding/binary"
"io"
"io/fs"
"math"
"path"
"sync"

"github.com/pkg/errors"
)

type gozipArchiveEntry struct {
Expand Down Expand Up @@ -164,6 +167,42 @@ func (e gozipArchiveEntry) StreamCompressed(w io.Writer) (int64, error) {
return io.Copy(w, f)
}

func (e gozipArchiveEntry) StreamCompressedGzip(w io.Writer) (int64, error) {
if e.file.Method != zip.Deflate {
return -1, errors.New("not a compressed resource")
}
if e.file.UncompressedSize64 > math.MaxUint32 {
return -1, errors.New("uncompressed size > 2^32 too large for GZIP")
}
f, err := e.file.OpenRaw()
if err != nil {
return -1, err
}

// Header
buf := [10]byte{0: gzipID1, 1: gzipID2, 2: gzipDeflate, 9: 255}
// No extra, no name, no comment, no mod time, no compress level hint, unknown OS

n, err := w.Write(buf[:10])
if err != nil {
return -1, errors.Wrap(err, "failed to write GZIP header")
}

nn, err := io.Copy(w, f)
if err != nil {
return int64(n), errors.Wrap(err, "failed copying deflated bytes")
}

// Trailer
binary.LittleEndian.PutUint32(buf[:4], e.file.CRC32)
binary.LittleEndian.PutUint32(buf[4:8], uint32(e.file.UncompressedSize64))
nnn, err := w.Write(buf[:8])
if err != nil {
return int64(n) + nn, errors.Wrap(err, "failed writing GZIP trailer")
}
return int64(n) + nn + int64(nnn), nil
}

func (e gozipArchiveEntry) ReadCompressed() ([]byte, error) {
if e.file.Method != zip.Deflate {
return nil, errors.New("not a compressed resource")
Expand All @@ -182,6 +221,40 @@ func (e gozipArchiveEntry) ReadCompressed() ([]byte, error) {
return compressedData, nil
}

func (e gozipArchiveEntry) ReadCompressedGzip() ([]byte, error) {
if e.file.Method != zip.Deflate {
return nil, errors.New("not a compressed resource")
}
if e.file.UncompressedSize64 > math.MaxUint32 {
return nil, errors.New("uncompressed size > 2^32 too large for GZIP")
}
f, err := e.file.OpenRaw()
if err != nil {
return nil, err
}

compressedData := make([]byte, e.file.CompressedSize64+GzipWrapperLength) // Size of file + header + trailer

// Deflated data
_, err = io.ReadAtLeast(f, compressedData[10:], int(e.file.CompressedSize64))
if err != nil {
return nil, err
}

// Header
compressedData[0] = gzipID1
compressedData[1] = gzipID2
compressedData[2] = gzipDeflate
compressedData[9] = 255
// No extra, no name, no comment, no mod time, no compress level hint, unknown OS

// Trailer
binary.LittleEndian.PutUint32(compressedData[10+e.file.CompressedSize64:], e.file.CRC32)
binary.LittleEndian.PutUint32(compressedData[10+e.file.CompressedSize64+4:], uint32(e.file.UncompressedSize64))

return compressedData, nil
}

// An archive from a zip file using go's stdlib
type gozipArchive struct {
zip *zip.Reader
Expand Down
12 changes: 12 additions & 0 deletions pkg/archive/gzip.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package archive

import "math"

const (
gzipID1 = 0x1f
gzipID2 = 0x8b
gzipDeflate = 8
)

const GzipWrapperLength = 18
const GzipMaxLength = math.MaxUint32
18 changes: 18 additions & 0 deletions pkg/fetcher/fetcher_archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,15 @@ func (r *entryResource) StreamCompressed(w io.Writer) (int64, *ResourceError) {
return -1, Other(err)
}

// StreamCompressedGzip implements CompressedResource
func (r *entryResource) StreamCompressedGzip(w io.Writer) (int64, *ResourceError) {
i, err := r.entry.StreamCompressedGzip(w)
if err == nil {
return i, nil
}
return -1, Other(err)
}

// ReadCompressed implements CompressedResource
func (r *entryResource) ReadCompressed() ([]byte, *ResourceError) {
i, err := r.entry.ReadCompressed()
Expand All @@ -180,6 +189,15 @@ func (r *entryResource) ReadCompressed() ([]byte, *ResourceError) {
return nil, Other(err)
}

// ReadCompressedGzip implements CompressedResource
func (r *entryResource) ReadCompressedGzip() ([]byte, *ResourceError) {
i, err := r.entry.ReadCompressedGzip()
if err == nil {
return i, nil
}
return nil, Other(err)
}

// Length implements Resource
func (r *entryResource) Length() (int64, *ResourceError) {
return int64(r.entry.Length()), nil
Expand Down
18 changes: 18 additions & 0 deletions pkg/fetcher/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,15 @@ func (r ProxyResource) StreamCompressed(w io.Writer) (int64, *ResourceError) {
return cres.StreamCompressed(w)
}

// StreamCompressedGzip implements CompressedResource
func (r ProxyResource) StreamCompressedGzip(w io.Writer) (int64, *ResourceError) {
cres, ok := r.Res.(CompressedResource)
if !ok {
return -1, Other(errors.New("resource is not compressed"))
}
return cres.StreamCompressedGzip(w)
}

// ReadCompressed implements CompressedResource
func (r ProxyResource) ReadCompressed() ([]byte, *ResourceError) {
cres, ok := r.Res.(CompressedResource)
Expand All @@ -403,6 +412,15 @@ func (r ProxyResource) ReadCompressed() ([]byte, *ResourceError) {
return cres.ReadCompressed()
}

// ReadCompressedGzip implements CompressedResource
func (r ProxyResource) ReadCompressedGzip() ([]byte, *ResourceError) {
cres, ok := r.Res.(CompressedResource)
if !ok {
return nil, Other(errors.New("resource is not compressed"))
}
return cres.ReadCompressedGzip()
}

/**
* Transforms the bytes of [resource] on-the-fly.
*
Expand Down
2 changes: 2 additions & 0 deletions pkg/fetcher/traits.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@ type CompressedResource interface {
CompressedAs(compressionMethod archive.CompressionMethod) bool
CompressedLength() int64
StreamCompressed(w io.Writer) (int64, *ResourceError)
StreamCompressedGzip(w io.Writer) (int64, *ResourceError)
ReadCompressed() ([]byte, *ResourceError)
ReadCompressedGzip() ([]byte, *ResourceError)
}
22 changes: 21 additions & 1 deletion pkg/parser/epub/deobfuscator.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,18 @@ func (d DeobfuscatingResource) StreamCompressed(w io.Writer) (int64, *fetcher.Re
return d.ProxyResource.StreamCompressed(w)
}

// StreamCompressedGzip implements CompressedResource
func (d DeobfuscatingResource) StreamCompressedGzip(w io.Writer) (int64, *fetcher.ResourceError) {
_, v := d.obfuscation()
if v > 0 {
return 0, fetcher.Other(errors.New("cannot stream compressed resource when obfuscated"))
}

return d.ProxyResource.StreamCompressedGzip(w)
}

// ReadCompressed implements CompressedResource
func (d DeobfuscatingResource) ReadCompressed(w io.Writer) ([]byte, *fetcher.ResourceError) {
func (d DeobfuscatingResource) ReadCompressed() ([]byte, *fetcher.ResourceError) {
_, v := d.obfuscation()
if v > 0 {
return nil, fetcher.Other(errors.New("cannot read compressed resource when obfuscated"))
Expand All @@ -184,6 +194,16 @@ func (d DeobfuscatingResource) ReadCompressed(w io.Writer) ([]byte, *fetcher.Res
return d.ProxyResource.ReadCompressed()
}

// ReadCompressedGzip implements CompressedResource
func (d DeobfuscatingResource) ReadCompressedGzip() ([]byte, *fetcher.ResourceError) {
_, v := d.obfuscation()
if v > 0 {
return nil, fetcher.Other(errors.New("cannot read compressed resource when obfuscated"))
}

return d.ProxyResource.ReadCompressedGzip()
}

func (d DeobfuscatingResource) getHashKeyAdobe() []byte {
hexbytes, _ := hex.DecodeString(
strings.Replace(
Expand Down

0 comments on commit 232b8c3

Please sign in to comment.