extends SceneTree
## GATE — GltfPacker: a .gltf (JSON + side files) packed into one self-contained .glb
## at import, so the room only ever receives .glb bytes.
## G1 Godot's own multi-file .gltf packs, passes GlbCheck, and Godot loads it with the
## same triangles + textures as the .glb export of the same scene
## G2 base64 data: URIs pack too
## G3 a percent-encoded file name resolves
## G4 refused: '..', absolute paths, drive letters, URLs, missing files, a symlink
## out of the folder, non-base64 data: URIs
##
## Files go under user://gate_gltf_packer/ and are removed at the end.
##
## Run: CI=true "$HYPER_TPS_GODOT" --headless --path . --script res://scenes/test/gate_gltf_packer.gd
const DIR := "user://gate_gltf_packer"
var ok := true
var checks := 0
var _frame := 0
func _p(c: bool, m: String, got: Variant = "") -> void:
checks += 1
print(("ok " if c else "FAIL: ") + m + ("" if str(got) == "" else " (" + str(got) + ")"))
if not c:
ok = false
func _process(_d: float) -> bool:
_frame += 1
if _frame < 3:
return false
_run()
_rm(ProjectSettings.globalize_path(DIR))
print("\n%d checks — %s" % [checks, "PASS" if ok else "FAIL"])
quit(0 if ok else 1)
return true
## Remove ONLY this gate's own scratch tree.
func _rm(path: String) -> void:
if not path.contains("gate_gltf_packer"):
return
var d := DirAccess.open(path)
if d == null:
return
for f in d.get_files():
d.remove(f)
for sub in d.get_directories():
_rm(path.path_join(sub))
DirAccess.remove_absolute(path)
func _scene() -> Node3D:
var root := Node3D.new()
root.name = "Model"
var mi := MeshInstance3D.new()
mi.name = "Box"
mi.mesh = BoxMesh.new()
var img := Image.create_empty(16, 16, false, Image.FORMAT_RGBA8)
img.fill(Color(0.2, 0.6, 0.9))
var mat := StandardMaterial3D.new()
mat.albedo_texture = ImageTexture.create_from_image(img)
mi.material_override = mat
root.add_child(mi)
mi.owner = root
return root
func _json(path: String) -> Dictionary:
return JSON.parse_string(FileAccess.get_file_as_string(path))
func _write_json(path: String, j: Dictionary) -> void:
var f := FileAccess.open(path, FileAccess.WRITE)
f.store_string(JSON.stringify(j))
f.close()
func _refused(j: Dictionary, base: String, label: String, want: String) -> void:
var r := GltfPacker.pack(j, base)
_p(not bool(r["ok"]) and String(r["reason"]).contains(want), label, r["reason"])
func _run() -> void:
var dir := ProjectSettings.globalize_path(DIR)
DirAccess.make_dir_recursive_absolute(dir + "/model")
var gltf := dir + "/model/box.gltf"
var root := _scene()
var doc := GLTFDocument.new()
var st := GLTFState.new()
_p(doc.append_from_scene(root, st) == OK, "exporter builds the scene")
_p(doc.write_to_filesystem(st, gltf) == OK, "Godot writes a multi-file .gltf")
var st2 := GLTFState.new()
doc.append_from_scene(root, st2)
var ref := GlbCheck.check(doc.generate_buffer(st2))
root.free()
var src := _json(gltf)
var side: Array = []
for b: Variant in src.get("buffers", []):
if (b as Dictionary).has("uri"):
side.append(String(b["uri"]))
for im: Variant in src.get("images", []):
if (im as Dictionary).has("uri"):
side.append(String(im["uri"]))
_p(side.size() >= 2, " with side files (a .bin and a texture)", side)
# ---- G1 ----
var r := GltfPacker.pack_file(gltf)
_p(bool(r["ok"]), "G1 the .gltf packs", r["reason"])
var v := GlbCheck.check(r["bytes"])
_p(bool(v["ok"]), "G1 the result passes GlbCheck (no uri left)", v["reason"])
_p(int(v["triangles"]) == int(ref["triangles"]), "G1 same triangles as the .glb export",
"%d vs %d" % [int(v["triangles"]), int(ref["triangles"])])
_p((v["json"] as Dictionary).get("images", []).size() == 1, "G1 the texture came along, as a bufferView")
var lst := GLTFState.new()
_p(GLTFDocument.new().append_from_buffer(r["bytes"], "", lst) == OK and lst.get_images().size() == 1,
"G1 Godot loads the packed .glb with its texture")
# ---- G2: everything as base64 data: URIs ----
var emb := src.duplicate(true)
for b: Variant in emb["buffers"]:
var raw := FileAccess.get_file_as_bytes(dir + "/model/" + String(b["uri"]).uri_decode())
b["uri"] = "data:application/octet-stream;base64," + Marshalls.raw_to_base64(raw)
for im: Variant in emb["images"]:
var raw2 := FileAccess.get_file_as_bytes(dir + "/model/" + String(im["uri"]).uri_decode())
im["uri"] = "data:image/png;base64," + Marshalls.raw_to_base64(raw2)
im.erase("mimeType")
var r2 := GltfPacker.pack(emb, dir + "/model")
_p(bool(r2["ok"]) and bool(GlbCheck.check(r2["bytes"])["ok"]), "G2 data: URIs pack and pass", r2["reason"])
# ---- G3: a percent-encoded name ----
var img_uri := String((src["images"] as Array)[0]["uri"])
DirAccess.copy_absolute(dir + "/model/" + img_uri.uri_decode(), dir + "/model/my tex.png")
var pct := src.duplicate(true)
(pct["images"] as Array)[0]["uri"] = "my%20tex.png"
_p(bool(GltfPacker.pack(pct, dir + "/model")["ok"]), "G3 'my%20tex.png' resolves to 'my tex.png'")
# ---- G4: everything that would read outside the folder ----
var bin_uri := String((src["buffers"] as Array)[0]["uri"])
DirAccess.copy_absolute(dir + "/model/" + bin_uri.uri_decode(), dir + "/outside.bin")
for bad: Array in [["../outside.bin", "'..'"], ["sub/../../outside.bin", "'..'"],
["/etc/hostname", "absolute"], ["C:\\Windows\\win.ini", "absolute"],
["http://example.com/x.bin", "absolute"], ["file:///etc/hostname", "absolute"],
["nothere.bin", "missing"]]:
var j := src.duplicate(true)
(j["buffers"] as Array)[0]["uri"] = bad[0]
_refused(j, dir + "/model", "G4 buffer uri %s refused" % bad[0], String(bad[1]))
var dj := src.duplicate(true)
(dj["buffers"] as Array)[0]["uri"] = "data:application/octet-stream,rawbytes"
_refused(dj, dir + "/model", "G4 a non-base64 data: URI is refused", "base64")
# A symlink INSIDE the folder that points outside it.
OS.execute("ln", PackedStringArray(["-s", dir, dir + "/model/escape"]))
if DirAccess.open(dir + "/model").is_link("escape"):
var lj := src.duplicate(true)
(lj["buffers"] as Array)[0]["uri"] = "escape/outside.bin"
_refused(lj, dir + "/model", "G4 a path through a symlink is refused", "link")
DirAccess.remove_absolute(dir + "/model/escape")
else:
_p(false, "G4 could not make a symlink to test with")