qmuntal/gltf

View on GitHub
decoder.go

Summary

Maintainability
A
0 mins
Test Coverage
package gltf

import (
    "bufio"
    "bytes"
    "encoding/binary"
    "encoding/json"
    "errors"
    "fmt"
    "io"
    "io/fs"
    "net/url"
    "os"
    "path/filepath"
    "strings"
)

// Open will open a glTF or GLB file specified by name and return the Document.
func Open(name string) (*Document, error) {
    f, err := os.Open(name)
    if err != nil {
        return nil, err
    }
    defer f.Close()
    dec := NewDecoderFS(f, os.DirFS(filepath.Dir(name)))
    doc := new(Document)
    if err = dec.Decode(doc); err != nil {
        doc = nil
    }
    return doc, err
}

// A Decoder reads and decodes glTF and GLB values from an input stream.
//
// Only buffers with relative URIs will be read from Fsys.
// Fsys is called to read external resources.
type Decoder struct {
    Fsys fs.FS
    r    *bufio.Reader
}

// NewDecoder returns a new decoder that reads from r.
func NewDecoder(r io.Reader) *Decoder {
    return &Decoder{
        r: bufio.NewReader(r),
    }
}

// NewDecoder returns a new decoder that reads from r.
func NewDecoderFS(r io.Reader, fsys fs.FS) *Decoder {
    return &Decoder{
        Fsys: fsys,
        r:    bufio.NewReader(r),
    }
}

// Decode reads the next JSON-encoded value from its
// input and stores it in the value pointed to by doc.
func (d *Decoder) Decode(doc *Document) error {
    isBinary, err := d.decodeDocument(doc)
    if err != nil {
        return err
    }

    for _, b := range doc.Buffers {
        if !b.IsEmbeddedResource() {
            if uri, ok := sanitizeURI(b.URI); ok {
                b.URI = uri
            }
        }
    }
    for _, im := range doc.Images {
        if !im.IsEmbeddedResource() {
            if uri, ok := sanitizeURI(im.URI); ok {
                im.URI = uri
            }
        }
    }

    var externalBufferIndex = 0
    if isBinary && len(doc.Buffers) > 0 && doc.Buffers[0].URI == "" {
        externalBufferIndex = 1
        if err := d.decodeBinaryBuffer(doc.Buffers[0]); err != nil {
            return err
        }
    }
    for i := externalBufferIndex; i < len(doc.Buffers); i++ {
        if err := d.decodeBuffer(doc.Buffers[i]); err != nil {
            return err
        }
    }
    return nil
}

func (d *Decoder) decodeDocument(doc *Document) (bool, error) {
    glbHeader, err := d.readGLBHeader()
    if err != nil {
        return false, err
    }
    var (
        jd       *json.Decoder
        isBinary bool
    )
    if glbHeader != nil {
        jd = json.NewDecoder(&io.LimitedReader{R: d.r, N: int64(glbHeader.JSONHeader.Length)})
        isBinary = true
    } else {
        jd = json.NewDecoder(d.r)
        isBinary = false
    }

    err = jd.Decode(doc)
    return isBinary, err
}

func (d *Decoder) readGLBHeader() (*glbHeader, error) {
    var header glbHeader
    chunk, err := d.r.Peek(binary.Size(header))
    if err != nil {
        return nil, nil
    }
    r := bytes.NewReader(chunk)
    binary.Read(r, binary.LittleEndian, &header)
    if header.Magic != glbHeaderMagic {
        return nil, nil
    }
    d.r.Read(chunk)
    return &header, d.validateGLBHeader(&header)
}

func (d *Decoder) validateGLBHeader(header *glbHeader) error {
    if header.JSONHeader.Type != glbChunkJSON || (header.JSONHeader.Length+uint32(binary.Size(header))) > header.Length {
        return errors.New("gltf: Invalid GLB JSON header")
    }
    return nil
}

func (d *Decoder) chunkHeader() (*chunkHeader, error) {
    var header chunkHeader
    if err := binary.Read(d.r, binary.LittleEndian, &header); err != nil {
        return nil, err
    }
    return &header, nil
}

func (d *Decoder) decodeBuffer(buffer *Buffer) error {
    if err := d.validateBuffer(buffer); err != nil {
        return err
    }
    if buffer.URI == "" {
        return errors.New("gltf: buffer without URI")
    }
    var err error
    if buffer.IsEmbeddedResource() {
        buffer.Data, err = buffer.marshalData()
    } else {
        err = validateBufferURI(buffer.URI)
        if err == nil && d.Fsys != nil {
            buffer.Data, err = fs.ReadFile(d.Fsys, buffer.URI)
            if len(buffer.Data) > int(buffer.ByteLength) {
                buffer.Data = buffer.Data[:buffer.ByteLength:buffer.ByteLength]
            }
        }
    }
    if err != nil {
        buffer.Data = nil
    }
    return err
}

func (d *Decoder) decodeBinaryBuffer(buffer *Buffer) error {
    if err := d.validateBuffer(buffer); err != nil {
        return err
    }
    header, err := d.chunkHeader()
    if err != nil {
        return err
    }
    if header.Type != glbChunkBIN || header.Length < buffer.ByteLength {
        return errors.New("gltf: Invalid GLB BIN header")
    }
    buffer.Data = make([]byte, buffer.ByteLength)
    _, err = io.ReadFull(d.r, buffer.Data)
    return err
}

func (d *Decoder) validateBuffer(buffer *Buffer) error {
    if buffer.ByteLength == 0 {
        return errors.New("gltf: Invalid buffer.byteLength value = 0")
    }
    return nil
}

func validateBufferURI(uri string) error {
    if u, err := url.Parse(uri); err == nil && u.Scheme != "" {
        return nil
    }
    if !filepath.IsLocal(uri) {
        return fmt.Errorf("gltf: Invalid buffer.uri value '%s'", uri)
    }
    return nil
}

func sanitizeURI(uri string) (string, bool) {
    uri = strings.Replace(uri, "\\", "/", -1)
    uri = strings.Replace(uri, "/./", "/", -1)
    uri = strings.TrimPrefix(uri, "./")
    u, err := url.Parse(uri)
    if err != nil {
        return "", false
    }
    if u.Scheme == "" {
        // URI should always be decoded before using it in a file path.
        uri, err = url.PathUnescape(uri)
        if err != nil {
            return "", false
        }
    } else {
        uri = u.String()
    }
    return uri, true
}