package imageasset
import (
"container/list"
"context"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"net/http"
"path/filepath"
"sync"
"time"
"github.com/pancsta/d2/lib/localfile"
"github.com/pancsta/d2/lib/netpolicy"
)
const maxConcurrentResolutions = 16
const maxResponseHeaderBytes int64 = 64 << 10
type Kind uint8
const (
KindRaster Kind = iota + 1
KindSVG
)
var ErrUnavailable = errors .New ("imageasset: source unavailable" )
type unavailableError struct {
err error
}
func (e *unavailableError ) Error () string { return e .err .Error() }
func (e *unavailableError ) Unwrap () error { return e .err }
func (e *unavailableError ) Is (target error ) bool {
return target == ErrUnavailable
}
func unavailable(err error ) error {
if err == nil || errors .Is (err , ErrUnavailable ) {
return err
}
return &unavailableError {err : err }
}
func (k Kind ) String () string {
switch k {
case KindRaster :
return "raster"
case KindSVG :
return "svg"
default :
return fmt .Sprintf ("Kind(%d)" , k )
}
}
type Limits struct {
MaxFetchedBytes int64
MaxEncodedBytes int64
MaxDecompressedBytes int64
MaxSVGBytes int64
MaxDecodedWidth int
MaxDecodedHeight int
MaxDecodedPixels int64
MaxAssets int
MaxCumulativeEncodedBytes int64
MaxCumulativeDecodedBytes int64
}
func (l Limits ) Validate () error {
if l .MaxFetchedBytes <= 0 || l .MaxEncodedBytes <= 0 || l .MaxDecompressedBytes <= 0 || l .MaxSVGBytes <= 0 ||
l .MaxDecodedWidth <= 0 || l .MaxDecodedHeight <= 0 || l .MaxDecodedPixels <= 0 ||
l .MaxAssets <= 0 || l .MaxCumulativeEncodedBytes <= 0 || l .MaxCumulativeDecodedBytes <= 0 {
return errors .New ("imageasset: every limit must be positive" )
}
return nil
}
type Resource struct {
kind Kind
mimeType string
data []byte
pixelWidth int
pixelHeight int
fetchedBytes int64
decompressedBytes int64
decodedBytes int64
}
func (r *Resource ) Kind () Kind { return r .kind }
func (r *Resource ) MIMEType () string { return r .mimeType }
func (r *Resource ) PixelWidth () int { return r .pixelWidth }
func (r *Resource ) PixelHeight () int { return r .pixelHeight }
func (r *Resource ) EncodedBytes () int64 { return int64 (len (r .data )) }
func (r *Resource ) DecodedBytes () int64 { return r .decodedBytes }
func (r *Resource ) fetchedByteCount () int64 { return r .fetchedBytes }
func (r *Resource ) decompressedByteCount () int64 { return r .decompressedBytes }
func (r *Resource ) cloneBytes () []byte { return append ([]byte (nil ), r .data ...) }
func (r *Resource ) BytesContext (ctx context .Context ) ([]byte , error ) {
if ctx == nil {
ctx = context .Background ()
}
if err := checkContext (ctx ); err != nil {
return nil , err
}
result := make ([]byte , len (r .data ))
const chunkBytes = 32 << 10
for offset := 0 ; offset < len (r .data ); offset += chunkBytes {
if err := checkContext (ctx ); err != nil {
return nil , err
}
end := offset + chunkBytes
if end > len (r .data ) {
end = len (r .data )
}
copy (result [offset :end ], r .data [offset :end ])
}
if err := checkContext (ctx ); err != nil {
return nil , err
}
return result , nil
}
func (r *Resource ) DataURIContext (ctx context .Context ) ([]byte , error ) {
if r == nil {
return nil , errors .New ("imageasset: nil resource" )
}
if ctx == nil {
ctx = context .Background ()
}
if err := checkContext (ctx ); err != nil {
return nil , err
}
prefix := []byte ("data:" + r .mimeType + ";base64," )
dataBytes := int64 (len (r .data ))
if dataBytes > (int64 (^uint64 (0 )>>1 )-2 )/4 *3 {
return nil , errors .New ("imageasset: data URI size overflows int64" )
}
encodedBytes := (dataBytes + 2 ) / 3 * 4
totalBytes := int64 (len (prefix )) + encodedBytes
maxInt := uint64 (^uint (0 ) >> 1 )
if totalBytes < 0 || uint64 (totalBytes ) > maxInt {
return nil , errors .New ("imageasset: data URI is not representable" )
}
result := make ([]byte , int (totalBytes ))
copy (result , prefix )
writeAt := len (prefix )
const chunkBytes = 48 << 10
for readAt := 0 ; readAt < len (r .data ); readAt += chunkBytes {
if err := checkContext (ctx ); err != nil {
return nil , err
}
end := readAt + chunkBytes
if end > len (r .data ) {
end = len (r .data )
}
base64 .StdEncoding .Encode (result [writeAt :], r .data [readAt :end ])
writeAt += base64 .StdEncoding .EncodedLen (end - readAt )
}
if err := checkContext (ctx ); err != nil {
return nil , err
}
if writeAt != len (result ) {
return nil , errors .New ("imageasset: data URI size preflight mismatch" )
}
return result , nil
}
type Cache interface {
Get (key string ) (*Resource , bool )
Put (key string , resource *Resource )
}
type MemoryCache struct {
mu sync .Mutex
entries map [string ]*cacheEntry
recent *list .List
maxEntries int
maxBytes int64
bytes int64
}
type cacheEntry struct {
key string
resource *Resource
element *list .Element
}
func NewMemoryCache (maxEntries int , maxBytes int64 ) (*MemoryCache , error ) {
if maxEntries <= 0 || maxBytes <= 0 {
return nil , errors .New ("imageasset: cache limits must be positive" )
}
return &MemoryCache {
entries : make (map [string ]*cacheEntry ),
recent : list .New (),
maxEntries : maxEntries ,
maxBytes : maxBytes ,
}, nil
}
func (c *MemoryCache ) Get (key string ) (*Resource , bool ) {
if c == nil || c .recent == nil {
return nil , false
}
c .mu .Lock ()
defer c .mu .Unlock ()
entry , ok := c .entries [key ]
if !ok {
return nil , false
}
c .recent .MoveToFront (entry .element )
return entry .resource , true
}
func (c *MemoryCache ) Put (key string , resource *Resource ) {
if c == nil || c .recent == nil || resource == nil {
return
}
c .mu .Lock ()
defer c .mu .Unlock ()
if int64 (len (resource .data )) > c .maxBytes {
if old , ok := c .entries [key ]; ok {
delete (c .entries , key )
c .recent .Remove (old .element )
c .bytes -= int64 (len (old .resource .data ))
}
return
}
if old , ok := c .entries [key ]; ok {
delete (c .entries , key )
c .bytes -= int64 (len (old .resource .data ))
c .recent .Remove (old .element )
}
resourceBytes := int64 (len (resource .data ))
for len (c .entries ) >= c .maxEntries || resourceBytes > c .maxBytes -c .bytes {
oldestElement := c .recent .Back ()
if oldestElement == nil {
break
}
oldest := oldestElement .Value .(*cacheEntry )
delete (c .entries , oldest .key )
c .recent .Remove (oldestElement )
c .bytes -= int64 (len (oldest .resource .data ))
}
entry := &cacheEntry {key : key , resource : resource }
entry .element = c .recent .PushFront (entry )
c .entries [key ] = entry
c .bytes += resourceBytes
}
type Options struct {
BaseDir string
LocalFiles localfile .Policy
HTTPClient *http .Client
NetworkPolicy netpolicy .Policy
Cache Cache
CacheNamespace string
Limits Limits
}
type Resolver struct {
baseDir string
localFiles localfile .Policy
client *http .Client
ownsHTTPTransport bool
cache Cache
cachePrefix string
limits Limits
mu sync .Mutex
assetCount int
cumulativeEncodedBytes int64
cumulativeDecodedBytes int64
resolvedSources map [string ]*Resource
inflight map [string ]*resolveCall
resolveSlots chan struct {}
}
func New (options Options ) (*Resolver , error ) {
if err := options .Limits .Validate (); err != nil {
return nil , err
}
if options .Cache != nil && options .CacheNamespace == "" {
return nil , errors .New ("imageasset: CacheNamespace is required when Cache is set" )
}
baseDir := options .BaseDir
if baseDir == "" {
if root , ok := options .LocalFiles .RootPath (); ok {
baseDir = root
} else {
baseDir = "."
}
}
absolute , err := filepath .Abs (baseDir )
if err != nil {
return nil , fmt .Errorf ("imageasset: resolve base directory %q: %w" , baseDir , err )
}
baseDir = filepath .Clean (absolute )
defaultTransport := boundedDefaultTransport (http .DefaultTransport )
client := &http .Client {Transport : defaultTransport , Timeout : time .Minute }
ownsHTTPTransport := true
if options .HTTPClient != nil {
clone := *options .HTTPClient
if clone .Transport == nil {
clone .Transport = defaultTransport
} else if options .NetworkPolicy .AllowPrivateNetworks {
ownsHTTPTransport = false
}
client = &clone
}
client , err = netpolicy .NewHTTPClient (client , options .NetworkPolicy )
if err != nil {
return nil , fmt .Errorf ("imageasset: configure HTTP network policy: %w" , err )
}
cachePrefix := ""
if options .Cache != nil {
networkNamespace := "public-only"
if options .NetworkPolicy .AllowPrivateNetworks {
networkNamespace = "private-network"
}
namespaceHash := sha256 .Sum256 ([]byte (options .CacheNamespace + "\x00network-policy:" + networkNamespace ))
cachePrefix = fmt .Sprintf ("namespace:%x:" , namespaceHash )
}
return &Resolver {
baseDir : baseDir ,
localFiles : options .LocalFiles ,
client : client ,
ownsHTTPTransport : ownsHTTPTransport ,
cache : options .Cache ,
cachePrefix : cachePrefix ,
limits : options .Limits ,
resolvedSources : make (map [string ]*Resource ),
inflight : make (map [string ]*resolveCall ),
resolveSlots : make (chan struct {}, maxConcurrentResolutions ),
}, nil
}
func (r *Resolver ) CloseIdleConnections () {
if r == nil || r .client == nil || !r .ownsHTTPTransport {
return
}
r .client .CloseIdleConnections ()
}
func boundedDefaultTransport(base http .RoundTripper ) *http .Transport {
if transport , ok := base .(*http .Transport ); ok {
clone := transport .Clone ()
clone .MaxResponseHeaderBytes = maxResponseHeaderBytes
return clone
}
return &http .Transport {
Proxy : http .ProxyFromEnvironment ,
ForceAttemptHTTP2 : true ,
MaxIdleConns : 100 ,
IdleConnTimeout : 90 * time .Second ,
TLSHandshakeTimeout : 10 * time .Second ,
ExpectContinueTimeout : 1 * time .Second ,
MaxResponseHeaderBytes : maxResponseHeaderBytes ,
}
}
func (r *Resolver ) assetCountSnapshot () int {
r .mu .Lock ()
defer r .mu .Unlock ()
return r .assetCount
}
func (r *Resolver ) cumulativeEncodedBytesSnapshot () int64 {
r .mu .Lock ()
defer r .mu .Unlock ()
return r .cumulativeEncodedBytes
}
func (r *Resolver ) cumulativeDecodedBytesSnapshot () int64 {
r .mu .Lock ()
defer r .mu .Unlock ()
return r .cumulativeDecodedBytes
}
type SourceError struct {
Source string
Op string
Err error
}
func (e *SourceError ) Error () string {
return fmt .Sprintf ("imageasset: %s %q: %v" , e .Op , e .Source , e .Err )
}
func (e *SourceError ) Unwrap () error { return e .Err }
type LimitError struct {
Name string
Actual int64
Limit int64
}
func (e *LimitError ) Error () string {
return fmt .Sprintf ("%s %d exceeds limit %d" , e .Name , e .Actual , e .Limit )
}
func sourceError(source , op string , err error ) error {
if err == nil {
return nil
}
return &SourceError {Source : source , Op : op , Err : err }
}
func checkContext(ctx context .Context ) error {
select {
case <- ctx .Done ():
return ctx .Err ()
default :
return nil
}
}
The pages are generated with Golds v0.8.7 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .