Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(io): Handle duplicate URIs on read/write #1511

Merged
merged 1 commit into from
Sep 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion packages/core/src/io/reader-context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { JSONDocument } from '../json-document.js';
import type { Document } from '../document.js';
import type {
Accessor,
Animation,
Expand All @@ -13,6 +14,7 @@ import type {
TextureInfo,
} from '../properties/index.js';
import type { GLTF } from '../types/gltf.js';
import type { ILogger } from '../utils/logger.js';

/**
* Model class providing glTF Transform objects representing each definition in the glTF file, used
Expand All @@ -22,6 +24,11 @@ import type { GLTF } from '../types/gltf.js';
* @hidden
*/
export class ReaderContext {
public readonly document: Document;
// TODO(v5): Rename to jsonDocument;
public readonly jsonDoc: JSONDocument;
private readonly logger: ILogger;

public buffers: Buffer[] = [];
public bufferViews: Uint8Array[] = [];
public bufferViewBuffers: Buffer[] = [];
Expand All @@ -36,7 +43,13 @@ export class ReaderContext {
public animations: Animation[] = [];
public scenes: Scene[] = [];

constructor(public readonly jsonDoc: JSONDocument) {}
private _usedURIs = new Set<string>();

constructor(document: Document, jsonDoc: JSONDocument) {
this.document = document;
this.jsonDoc = jsonDoc;
this.logger = document.getLogger();
}

public setTextureInfo(textureInfo: TextureInfo, textureInfoDef: GLTF.ITextureInfo): void {
this.textureInfos.set(textureInfo, textureInfoDef);
Expand Down Expand Up @@ -67,4 +80,15 @@ export class ReaderContext {
textureInfo.setWrapT(samplerDef.wrapT);
}
}

public requestURI(uri: string): string {
// https://github.com/KhronosGroup/glTF/issues/2446
if (this._usedURIs.has(uri)) {
this.logger.warn(`Duplicate resource URI, "${uri}".`);
return '';
}

this._usedURIs.add(uri);
return uri;
}
}
6 changes: 3 additions & 3 deletions packages/core/src/io/reader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export class GLTFReader {

/* Reader context. */

const context = new ReaderContext(jsonDoc);
const context = new ReaderContext(document, jsonDoc);

/** Asset. */

Expand Down Expand Up @@ -98,7 +98,7 @@ export class GLTFReader {
if (bufferDef.extras) buffer.setExtras(bufferDef.extras);

if (bufferDef.uri && bufferDef.uri.indexOf('__') !== 0) {
buffer.setURI(bufferDef.uri);
buffer.setURI(context.requestURI(bufferDef.uri));
}

return buffer;
Expand Down Expand Up @@ -172,7 +172,7 @@ export class GLTFReader {
} else if (imageDef.uri !== undefined) {
texture.setImage(jsonDoc.resources[imageDef.uri]);
if (imageDef.uri.indexOf('__') !== 0) {
texture.setURI(imageDef.uri);
texture.setURI(context.requestURI(imageDef.uri));
}
}

Expand Down
18 changes: 17 additions & 1 deletion packages/core/src/io/writer-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,26 @@ export class WriterContext {
} else {
const extension = ImageUtils.mimeTypeToExtension(texture.getMimeType());
imageDef.uri = this.imageURIGenerator.createURI(texture, extension);
this.jsonDoc.resources[imageDef.uri] = data;
this.assignResourceURI(imageDef.uri, data);
}
}

public assignResourceURI(uri: string, data: Uint8Array): void {
const resources = this.jsonDoc.resources;

// https://github.com/KhronosGroup/glTF/issues/2446
if (uri in resources && data !== resources[uri]) {
throw new Error(`Resource URI "${uri}" already assigned to different data.`);
}

if (uri in resources) {
this.logger.warn(`Duplicate resource URI, "${uri}".`);
return;
}

resources[uri] = data;
}

/**
* Returns implicit usage type of the given accessor, related to grouping accessors into
* buffer views. Usage is a superset of buffer view target, including ARRAY_BUFFER and
Expand Down
10 changes: 6 additions & 4 deletions packages/core/src/io/writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,9 +437,11 @@ export class GLTFWriter {
.filter((extension) => extension.prewriteTypes.includes(PropertyType.BUFFER))
.forEach((extension) => extension.prewrite(context, PropertyType.BUFFER));

const hasBinaryResources =
root.listAccessors().length > 0 || root.listTextures().length > 0 || context.otherBufferViews.size > 0;
if (hasBinaryResources && root.listBuffers().length === 0) {
const needsBuffer =
root.listAccessors().length > 0 ||
context.otherBufferViews.size > 0 ||
(root.listTextures().length > 0 && options.format === Format.GLB);
if (needsBuffer && root.listBuffers().length === 0) {
throw new Error('Buffer required for Document resources, but none was found.');
}

Expand Down Expand Up @@ -552,7 +554,7 @@ export class GLTFWriter {

// Write buffer views to buffer.
bufferDef.byteLength = bufferByteLength;
jsonDoc.resources[uri] = BufferUtils.concat(buffers);
context.assignResourceURI(uri, BufferUtils.concat(buffers));
}

json.buffers!.push(bufferDef);
Expand Down
85 changes: 85 additions & 0 deletions packages/core/test/io/platform-io.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,88 @@ test('glb with unknown chunk', async (t) => {
const node = document.getRoot().listNodes()[0];
t.is(node.getName(), 'RootNode', 'parses nodes');
});

test('read duplicate buffer URIs', async (t) => {
const io = await createPlatformIO();

// https://github.com/KhronosGroup/glTF/issues/2446
const jsonDocument: JSONDocument = {
json: {
asset: { version: '2.0' },
buffers: [
{ uri: 'scene.bin', byteLength: 20 },
{ uri: 'scene.bin', byteLength: 20 },
],
},
resources: { 'scene.bin': new Uint8Array(20) },
};

const document = await io.readJSON(jsonDocument);

const bufferURIs = document
.getRoot()
.listBuffers()
.map((buffer) => buffer.getURI());

t.deepEqual(bufferURIs, ['scene.bin', ''], 'removes duplicate buffer URIs');
});

test('read duplicate image URIs', async (t) => {
const io = await createPlatformIO();

// https://github.com/KhronosGroup/glTF/issues/2446
const image = new Uint8Array(20);
const jsonDocument: JSONDocument = {
json: {
asset: { version: '2.0' },
images: [{ uri: 'color.png' }, { uri: 'color.png' }],
},
resources: { 'color.png': image },
};

const document = await io.readJSON(jsonDocument);

const imageURIs = document
.getRoot()
.listTextures()
.map((texture) => texture.getURI());
const images = document
.getRoot()
.listTextures()
.map((texture) => texture.getImage());

t.deepEqual(imageURIs, ['color.png', ''], 'removes duplicate image URIs');
t.deepEqual(images, [image, image], 'loads duplicate image data');
});

test('write duplicate buffer URIs', async (t) => {
const io = await createPlatformIO();

// https://github.com/KhronosGroup/glTF/issues/2446
const document = new Document();
const buffer1 = document.createBuffer().setURI('scene.bin');
const buffer2 = document.createBuffer().setURI('scene.bin');
document.createAccessor().setBuffer(buffer1).setArray(new Float32Array(10).fill(1));
document.createAccessor().setBuffer(buffer2).setArray(new Float32Array(10).fill(2));

await t.throwsAsync(() => io.writeJSON(document), { message: /Resource URI/i }, 'duplicate buffer URIs');

buffer2.setURI('aux.bin');

t.truthy(await io.writeJSON(document), 'unique buffer URIs');
});

test('write duplicate image URIs', async (t) => {
const io = await createPlatformIO();

// https://github.com/KhronosGroup/glTF/issues/2446
const document = new Document();
const image1 = document.createTexture().setImage(new Uint8Array(2)).setURI('color.png');
document.createTexture().setImage(new Uint8Array(1)).setURI('color.png');

await t.throwsAsync(() => io.writeJSON(document), { message: /Resource URI/i }, 'duplicate image URIs');

image1.setURI('normal.png');

t.truthy(await io.writeJSON(document), 'unique image URIs');
});
Loading