feat(backend): zip page index with natural sort, zip-slip rejection, content hash
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
package bookfile
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var ErrNotZip = errors.New("not a readable zip")
|
||||
var ErrUnsafeZip = errors.New("unsafe zip entry")
|
||||
|
||||
func unsafeEntry(n string) bool {
|
||||
return strings.HasPrefix(n, "/") || strings.Contains(n, "..") || strings.ContainsRune(n, '\\')
|
||||
}
|
||||
|
||||
func isImage(name string) bool {
|
||||
switch strings.ToLower(path.Ext(name)) {
|
||||
case ".jpg", ".jpeg", ".png", ".webp", ".gif", ".avif":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func PageIndex(f io.ReaderAt, size int64) ([]string, error) {
|
||||
zr, err := zip.NewReader(f, size)
|
||||
if err != nil {
|
||||
return nil, ErrNotZip
|
||||
}
|
||||
var names []string
|
||||
for _, zf := range zr.File {
|
||||
if unsafeEntry(zf.Name) {
|
||||
return nil, fmt.Errorf("%w: %s", ErrUnsafeZip, zf.Name)
|
||||
}
|
||||
if isImage(zf.Name) {
|
||||
names = append(names, zf.Name)
|
||||
}
|
||||
}
|
||||
sort.Slice(names, func(i, j int) bool { return NaturalLess(names[i], names[j]) })
|
||||
return names, nil
|
||||
}
|
||||
|
||||
func ReadEntry(f io.ReaderAt, size int64, name string) ([]byte, error) {
|
||||
zr, err := zip.NewReader(f, size)
|
||||
if err != nil {
|
||||
return nil, ErrNotZip
|
||||
}
|
||||
for _, zf := range zr.File {
|
||||
if zf.Name == name {
|
||||
rc, err := zf.Open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rc.Close()
|
||||
return io.ReadAll(io.LimitReader(rc, 64<<20))
|
||||
}
|
||||
}
|
||||
return nil, errors.New("no such entry")
|
||||
}
|
||||
|
||||
func NaturalLess(a, b string) bool {
|
||||
i, j := 0, 0
|
||||
for i < len(a) && j < len(b) {
|
||||
da, db := isDigit(a[i]), isDigit(b[j])
|
||||
switch {
|
||||
case da && db:
|
||||
si, sj := i, j
|
||||
for i < len(a) && isDigit(a[i]) {
|
||||
i++
|
||||
}
|
||||
for j < len(b) && isDigit(b[j]) {
|
||||
j++
|
||||
}
|
||||
na, _ := strconv.Atoi(a[si:i])
|
||||
nb, _ := strconv.Atoi(b[sj:j])
|
||||
if na != nb {
|
||||
return na < nb
|
||||
}
|
||||
if a[si:i] != b[sj:j] {
|
||||
return a[si:i] < b[sj:j]
|
||||
}
|
||||
case !da && !db:
|
||||
if a[i] != b[j] {
|
||||
return a[i] < b[j]
|
||||
}
|
||||
i++
|
||||
j++
|
||||
default:
|
||||
return da // 数字段排在字母前
|
||||
}
|
||||
}
|
||||
return j < len(b)
|
||||
}
|
||||
|
||||
func isDigit(c byte) bool { return c >= '0' && c <= '9' }
|
||||
Reference in New Issue
Block a user