• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In
Build has been canceled!

qmuntal / gltf / 26519070502

27 May 2026 02:53PM UTC coverage: 88.601% (-2.7%) from 91.298%
26519070502

Pull #100

github

qmuntal
Tighten core glTF spec compliance
Pull Request #100: Tighten core glTF spec compliance

259 of 342 new or added lines in 6 files covered. (75.73%)

20 existing lines in 2 files now uncovered.

2705 of 3053 relevant lines covered (88.6%)

82.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.38
/decoder.go
1
package gltf
2

3
import (
4
        "bufio"
5
        "bytes"
6
        "encoding/binary"
7
        "encoding/json"
8
        "errors"
9
        "fmt"
10
        "io"
11
        "io/fs"
12
        "net/url"
13
        "os"
14
        "path/filepath"
15
        "strings"
16
)
17

18
// Open will open a glTF or GLB file specified by name and return the Document.
19
func Open(name string) (*Document, error) {
48✔
20
        f, err := os.Open(name)
48✔
21
        if err != nil {
54✔
22
                return nil, err
6✔
23
        }
6✔
24
        defer f.Close()
42✔
25
        dec := NewDecoderFS(f, os.DirFS(filepath.Dir(name)))
42✔
26
        doc := new(Document)
42✔
27
        if err = dec.Decode(doc); err != nil {
42✔
28
                doc = nil
×
29
        }
×
30
        return doc, err
42✔
31
}
32

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

42
// NewDecoder returns a new decoder that reads from r.
43
func NewDecoder(r io.Reader) *Decoder {
54✔
44
        return &Decoder{
54✔
45
                r: bufio.NewReader(r),
54✔
46
        }
54✔
47
}
54✔
48

49
// NewDecoder returns a new decoder that reads from r.
50
func NewDecoderFS(r io.Reader, fsys fs.FS) *Decoder {
456✔
51
        return &Decoder{
456✔
52
                Fsys: fsys,
456✔
53
                r:    bufio.NewReader(r),
456✔
54
        }
456✔
55
}
456✔
56

57
// Decode reads the next JSON-encoded value from its
58
// input and stores it in the value pointed to by doc.
59
func (d *Decoder) Decode(doc *Document) error {
462✔
60
        isBinary, err := d.decodeDocument(doc)
462✔
61
        if err != nil {
492✔
62
                return err
30✔
63
        }
30✔
64

65
        var externalBufferIndex = 0
432✔
66
        if isBinary && len(doc.Buffers) > 0 && doc.Buffers[0].URI == "" {
438✔
67
                externalBufferIndex = 1
6✔
68
                if err := d.decodeBinaryBuffer(doc.Buffers[0]); err != nil {
6✔
69
                        return err
×
70
                }
×
71
        }
72
        for i := externalBufferIndex; i < len(doc.Buffers); i++ {
474✔
73
                if err := d.decodeBuffer(doc.Buffers[i]); err != nil {
42✔
UNCOV
74
                        return err
×
UNCOV
75
                }
×
76
        }
77
        return nil
432✔
78
}
79

80
func (d *Decoder) decodeDocument(doc *Document) (bool, error) {
462✔
81
        glbHeader, err := d.readGLBHeader()
462✔
82
        if err != nil {
468✔
83
                return false, err
6✔
84
        }
6✔
85
        var (
456✔
86
                jd       *json.Decoder
456✔
87
                isBinary bool
456✔
88
        )
456✔
89
        if glbHeader != nil {
666✔
90
                jd = json.NewDecoder(&io.LimitedReader{R: d.r, N: int64(glbHeader.JSONHeader.Length)})
210✔
91
                isBinary = true
210✔
92
        } else {
456✔
93
                jd = json.NewDecoder(d.r)
246✔
94
                isBinary = false
246✔
95
        }
246✔
96

97
        err = jd.Decode(doc)
456✔
98
        return isBinary, err
456✔
99
}
100

101
func (d *Decoder) readGLBHeader() (*glbHeader, error) {
462✔
102
        var header glbHeader
462✔
103
        chunk, err := d.r.Peek(binary.Size(header))
462✔
104
        if err != nil {
474✔
105
                return nil, nil
12✔
106
        }
12✔
107
        r := bytes.NewReader(chunk)
450✔
108
        binary.Read(r, binary.LittleEndian, &header)
450✔
109
        if header.Magic != glbHeaderMagic {
684✔
110
                return nil, nil
234✔
111
        }
234✔
112
        d.r.Read(chunk)
216✔
113
        return &header, d.validateGLBHeader(&header)
216✔
114
}
115

116
func (d *Decoder) validateGLBHeader(header *glbHeader) error {
216✔
117
        if header.Version != 2 || header.JSONHeader.Type != glbChunkJSON || (header.JSONHeader.Length+uint32(binary.Size(header))) > header.Length {
222✔
118
                return errors.New("gltf: Invalid GLB JSON header")
6✔
119
        }
6✔
120
        return nil
210✔
121
}
122

123
func (d *Decoder) chunkHeader() (*chunkHeader, error) {
36✔
124
        var header chunkHeader
36✔
125
        if err := binary.Read(d.r, binary.LittleEndian, &header); err != nil {
42✔
126
                return nil, err
6✔
127
        }
6✔
128
        return &header, nil
30✔
129
}
130

131
func (d *Decoder) decodeBuffer(buffer *Buffer) error {
78✔
132
        if err := d.validateBuffer(buffer); err != nil {
84✔
133
                return err
6✔
134
        }
6✔
135
        if buffer.URI == "" {
78✔
136
                return errors.New("gltf: buffer without URI")
6✔
137
        }
6✔
138
        var err error
66✔
139
        if buffer.IsEmbeddedResource() {
84✔
140
                buffer.Data, err = buffer.marshalData()
18✔
141
        } else {
66✔
142
                uri := buffer.URI
48✔
143
                if sanitized, ok := sanitizeURI(uri); ok {
96✔
144
                        uri = sanitized
48✔
145
                }
48✔
146
                err = validateBufferURI(uri)
48✔
147
                if err == nil && d.Fsys != nil {
84✔
148
                        buffer.Data, err = fs.ReadFile(d.Fsys, uri)
36✔
149
                        if len(buffer.Data) > int(buffer.ByteLength) {
42✔
150
                                buffer.Data = buffer.Data[:buffer.ByteLength:buffer.ByteLength]
6✔
151
                        }
6✔
152
                }
153
        }
154
        if err != nil {
72✔
155
                buffer.Data = nil
6✔
156
        }
6✔
157
        return err
66✔
158
}
159

160
func (d *Decoder) decodeBinaryBuffer(buffer *Buffer) error {
42✔
161
        if err := d.validateBuffer(buffer); err != nil {
48✔
162
                return err
6✔
163
        }
6✔
164
        header, err := d.chunkHeader()
36✔
165
        if err != nil {
42✔
166
                return err
6✔
167
        }
6✔
168
        if header.Type != glbChunkBIN || header.Length < uint32(buffer.ByteLength) || header.Length > uint32(buffer.ByteLength+3) {
42✔
169
                return errors.New("gltf: Invalid GLB BIN header")
12✔
170
        }
12✔
171
        buffer.Data = make([]byte, buffer.ByteLength)
18✔
172
        _, err = io.ReadFull(d.r, buffer.Data)
18✔
173
        return err
18✔
174
}
175

176
func (d *Decoder) validateBuffer(buffer *Buffer) error {
120✔
177
        if buffer.ByteLength == 0 {
132✔
178
                return errors.New("gltf: Invalid buffer.byteLength value = 0")
12✔
179
        }
12✔
180
        return nil
108✔
181
}
182

183
func validateBufferURI(uri string) error {
108✔
184
        if u, err := url.Parse(uri); err == nil && u.Scheme != "" {
114✔
185
                return nil
6✔
186
        }
6✔
187
        if !filepath.IsLocal(uri) {
132✔
188
                return fmt.Errorf("gltf: Invalid buffer.uri value '%s'", uri)
30✔
189
        }
30✔
190
        return nil
72✔
191
}
192

193
func sanitizeURI(uri string) (string, bool) {
84✔
194
        uri = strings.Replace(uri, "\\", "/", -1)
84✔
195
        uri = strings.Replace(uri, "/./", "/", -1)
84✔
196
        uri = strings.TrimPrefix(uri, "./")
84✔
197
        u, err := url.Parse(uri)
84✔
198
        if err != nil {
84✔
199
                return "", false
×
200
        }
×
201
        if u.Scheme == "" {
162✔
202
                // URI should always be decoded before using it in a file path.
78✔
203
                uri, err = url.PathUnescape(uri)
78✔
204
                if err != nil {
78✔
205
                        return "", false
×
206
                }
×
207
        } else {
6✔
208
                uri = u.String()
6✔
209
        }
6✔
210
        return uri, true
84✔
211
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc