Disclosure: this article promotes Car Customizer and was prepared with AI assistance. The JavaScript and tests below are an independent teaching example, not the product's implementation, a security review, or a report of processing real photographs.
A file picker can tell you a name, a declared type, and a byte count. It is tempting to call a check of those fields image validation, especially when the interface is only meant to accept a few familiar formats. That name promises too much. A small metadata policy can explain why a selection needs attention, but it cannot tell you whether the bytes decode into a valid, appropriate image.
Give the result a deliberately modest name
The example below returns either reject or candidate. Candidate means only that the supplied record fits this example's metadata policy. It does not mean safe, genuine, private, decodable, or ready for a particular service. Naming that boundary explicitly helps prevent a later UI refactor from turning a limited convenience check into a reassuring badge that the underlying evidence cannot support.
The records in the tests are synthetic JavaScript objects. They are not browser File objects, downloaded photographs, decoder outputs, or responses from a production endpoint. That makes the example easy to run without collecting anyone's pictures. It also limits what the results establish: the tests exercise the function's decisions about these supplied values, not the truth of the values or the behavior of a real upload pipeline.
Write the policy before the implementation
This demonstration permits familiar JPEG, PNG, and WebP filename endings, a positive safe-integer byte count within an explicit budget, and a supported declared MIME type when one is supplied. A blank MIME string produces a note rather than a false content conclusion. The module checks agreement between two declarations when both are recognized; agreement is not evidence that either declaration describes the actual file contents.
The default budget is exactly ten million bytes. That is a choice for this example, not an interpretation of every interface that displays ten megabytes. Products may define or enforce a limit differently. The caller can pass a different positive safe integer, while invalid configuration throws. Keeping the budget explicit makes its boundary testable without silently changing a policy because a string, fraction, or unexpected number was passed in.
The filename rule expects a basename rather than a path and rejects control characters. It permits Unicode names and uses the last extension, while this particular policy rejects an extension-only name. These are organizational choices, not a claim to sanitize every operating system's filenames. The function does not rename a file, normalize its bytes, prevent every confusing Unicode presentation, or make a filesystem operation safe.
Keep the module small and its output focused
Save this module as car-file-intake.mjs. It has no network, filesystem, image-decoding, or browser-storage dependency. The return value contains decision codes and notes rather than copying the filename or extra supplied fields into an output object. That can reduce unnecessary propagation in a calling application, but it is not a privacy guarantee: a caller could still log the input or send the original file elsewhere.
export const DEFAULT_MAX_BYTES = 10_000_000;
const EXTENSIONS = new Map([
['jpg', 'image/jpeg'], ['jpeg', 'image/jpeg'],
['png', 'image/png'], ['webp', 'image/webp'],
]);
const MIMES = new Set(EXTENSIONS.values());
// Advisory metadata policy only. This does not inspect or decode any bytes.
export function inspectImageMetadata(record, maxBytes = DEFAULT_MAX_BYTES) {
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1) {
throw new RangeError('maxBytes must be a positive safe integer');
}
if (record === null || typeof record !== 'object' || Array.isArray(record)) {
return { decision: 'reject', issues: ['record_required'], notes: [] };
}
const prototype = Object.getPrototypeOf(record);
if (prototype !== Object.prototype && prototype !== null) {
return { decision: 'reject', issues: ['plain_record_required'], notes: [] };
}
const issues = [];
const notes = [];
const own = key => Object.hasOwn(record, key);
const name = own('filename') ? record.filename : undefined;
const size = own('bytes') ? record.bytes : undefined;
const mime = own('mime') ? record.mime : undefined;
let expectedMime;
if (typeof name !== 'string' || !name.trim()) {
issues.push('filename_required');
} else if (/[\x00-\x1f\x7f/\\]/.test(name)) {
issues.push('basename_required');
} else {
const match = /\S\.([^.]+)$/.exec(name.trim());
expectedMime = match && EXTENSIONS.get(match[1].toLowerCase());
if (!expectedMime) issues.push('extension_not_supported');
}
if (!Number.isSafeInteger(size) || size < 1) {
issues.push('positive_integer_size_required');
} else if (size > maxBytes) {
issues.push('size_over_limit');
}
if (typeof mime !== 'string') {
issues.push('mime_string_required');
} else {
const declared = mime.trim().toLowerCase();
if (!declared) notes.push('mime_not_supplied');
else if (!MIMES.has(declared)) issues.push('mime_not_supported');
else if (expectedMime && expectedMime !== declared) {
issues.push('extension_mime_disagree');
}
}
return { decision: issues.length ? 'reject' : 'candidate', issues, notes };
}
Put the boundary decisions into executable tests
Save the following suite as car-file-intake.test.mjs in the same directory. The tests cover ordinary candidates, exact and exceeded budgets, unsupported declarations, disagreement, missing MIME information, unsafe or coerced numbers, basename rules, Unicode, record shapes, and output behavior. Several tests group closely related synthetic inputs. The suite is intended to make these particular decisions visible, not to stand in for a comprehensive image-processing or adversarial JavaScript test program.
The frozen-input test checks that this implementation does not mutate an ordinary supplied record and does not echo its extra field or filename in the result. It does not establish that every JavaScript object is harmless to inspect. Getters and proxies can execute behavior when properties or prototypes are accessed. This module is not a sandbox for hostile objects, and the tests should not be described as proving such a property.
import test from 'node:test';
import assert from 'node:assert/strict';
import { inspectImageMetadata as inspect, DEFAULT_MAX_BYTES } from './car-file-intake.mjs';
const sample = (patch = {}) => ({ filename: 'concept.png', bytes: 80, mime: 'image/png', ...patch });
const issue = (patch, code) => assert.ok(inspect(sample(patch)).issues.includes(code));
test('PNG metadata is only a candidate', () => {
assert.deepEqual(inspect(sample()), { decision: 'candidate', issues: [], notes: [] });
});
test('JPG spelling', () => assert.equal(inspect(sample({ filename: 'look.jpg', mime: 'image/jpeg' })).decision, 'candidate'));
test('JPEG spelling', () => assert.equal(inspect(sample({ filename: 'look.jpeg', mime: 'image/jpeg' })).decision, 'candidate'));
test('WebP spelling', () => assert.equal(inspect(sample({ filename: 'look.webp', mime: 'image/webp' })).decision, 'candidate'));
test('case and surrounding whitespace are normalized', () => {
assert.equal(inspect(sample({ filename: ' LOOK.PNG ', mime: ' IMAGE/PNG ' })).decision, 'candidate');
});
test('Unicode basename is permitted', () => assert.equal(inspect(sample({ filename: '外観案-01.png' })).decision, 'candidate'));
test('multiple dots retain the final extension', () => assert.equal(inspect(sample({ filename: 'car.concept.v2.png' })).decision, 'candidate'));
test('exact decimal byte budget is permitted', () => assert.equal(inspect(sample({ bytes: DEFAULT_MAX_BYTES })).decision, 'candidate'));
test('one byte over budget', () => issue({ bytes: DEFAULT_MAX_BYTES + 1 }, 'size_over_limit'));
test('a custom byte budget is used', () => assert.equal(inspect(sample({ bytes: 81 }), 80).decision, 'reject'));
test('zero size is not accepted', () => issue({ bytes: 0 }, 'positive_integer_size_required'));
test('fractional size is not coerced', () => issue({ bytes: 1.5 }, 'positive_integer_size_required'));
test('string size is not coerced', () => issue({ bytes: '80' }, 'positive_integer_size_required'));
test('unsafe integer is not accepted', () => issue({ bytes: Number.MAX_SAFE_INTEGER + 1 }, 'positive_integer_size_required'));
test('NaN and infinity are not accepted', () => {
issue({ bytes: NaN }, 'positive_integer_size_required');
issue({ bytes: Infinity }, 'positive_integer_size_required');
});
test('missing MIME is a note, not content proof', () => {
assert.deepEqual(inspect(sample({ mime: '' })), { decision: 'candidate', issues: [], notes: ['mime_not_supplied'] });
});
test('non-string MIME is rejected', () => issue({ mime: null }, 'mime_string_required'));
test('unsupported MIME is rejected', () => issue({ mime: 'text/plain' }, 'mime_not_supported'));
test('disagreeing supported MIME is rejected', () => issue({ mime: 'image/jpeg' }, 'extension_mime_disagree'));
test('unsupported extension is rejected', () => issue({ filename: 'payload.exe' }, 'extension_not_supported'));
test('extension-only name is rejected by this policy', () => issue({ filename: '.png' }, 'extension_not_supported'));
test('blank filename is rejected', () => issue({ filename: ' ' }, 'filename_required'));
test('path and control characters are rejected', () => {
for (const filename of ['../look.png', 'folder\\look.png', 'look\u0000.png']) {
issue({ filename }, 'basename_required');
}
});
test('null, arrays, dates and functions are not records', () => {
for (const value of [null, [], new Date(0), () => {}]) assert.equal(inspect(value).decision, 'reject');
});
test('inherited fields are not accepted as a plain record', () => {
assert.deepEqual(inspect(Object.create(sample())), { decision: 'reject', issues: ['plain_record_required'], notes: [] });
});
test('null-prototype own-field record is supported', () => {
assert.equal(inspect(Object.assign(Object.create(null), sample())).decision, 'candidate');
});
test('input is not mutated and extra fields are not echoed', () => {
const original = Object.freeze(sample({ privateNote: 'synthetic-only' }));
const result = inspect(original);
assert.deepEqual(original, sample({ privateNote: 'synthetic-only' }));
assert.deepEqual(Object.keys(result), ['decision', 'issues', 'notes']);
assert.ok(!JSON.stringify(result).includes('concept.png'));
assert.ok(!JSON.stringify(result).includes('synthetic-only'));
});
test('invalid configuration throws instead of silently changing the budget', () => {
for (const max of [0, -1, 2.5, '80', Infinity]) assert.throws(() => inspect(sample(), max), RangeError);
});
Run the suite with a Node runtime that supports node:test and Object.hasOwn. The files should be together so the relative module import resolves. No third-party test framework or actual photo fixture is required for this example. Keeping the command close to the complete code makes the article easier to reproduce than a screenshot of a passing test runner without the exact implementation and cases that produced it.
node --test car-file-intake.test.mjs
The accompanying suite was executed locally for this article: twenty-eight tests passed and none failed. That result applies to the included synthetic fixtures and the implementation shown here. It is not evidence that Car Customizer uses this code, that its uploads were tested, or that a real JPEG, PNG, or WebP file was validated. No claim about image safety, decoding accuracy, or a production security boundary follows from the passing count.
Use the advice without promoting it into a certificate
In an interface, a reject result could explain which selection detail needs attention before a later step. A candidate result should lead to the next appropriate checks, not to a green label saying that an image is safe. Treat the absence of MIME information as the explicit note it is. Do not replace a missing declaration with an invented assurance merely because the filename looks familiar or the size is small.
A client-side convenience check should also not become the only enforcement point for a service. An application receiving actual bytes needs its own appropriate limits, content handling, and error behavior. The exact requirements depend on that application. This article deliberately does not supply a decoder or a security architecture. It demonstrates how to keep a modest policy comprehensible and testable without pretending that the rest of the system has been designed or verified.
Keep the stable issue codes separate from the sentences shown to users. A caller can translate a code into helpful local copy without changing the policy or including the selected filename in every message. If several issues are returned, decide deliberately how the interface will present them. Do not quietly promote a missing-MIME note into a successful content check just because the design only has room for one short status line.
A concrete product context, with the limits attached
Car Customizer at https://car-customizer.online/ is a browser-based AI tool for exploring car appearance concepts from a reference photograph. Its public interface advertises JPG, PNG, and WebP input with a displayed ten-megabyte limit. That is a useful example of why clear preparation messages matter. It is not evidence that the product shares this demonstration's decimal budget, extension rules, or treatment of a blank MIME string.
The public workflow describes visual directions involving paint, wheels, wraps, stance, body kits, and spoilers, along with comparing alternatives, version history, continuing edits, and downloads. Those are concept-organization features, not measurements or proof of component fit, legality, engineering behavior, or installation safety. A successful metadata check cannot add any of those properties to a generated image. It cannot establish permission to upload a photograph or remove the need to review visible personal identifiers either.
Preview generation uses credits. The current public pricing describes nine welcome credits once for new accounts rather than a recurring daily or monthly allowance, with paid subscriptions and one-time packs available for further credits. Check the current offer before generating. This article does not describe an unlimited free image service, and the local synthetic tests do not consume product credits or demonstrate any paid or free generation outcome.
Preserve the small promise when the code changes
When adapting the example, change the policy and its tests together. Decide explicitly whether a new extension is only another declaration, whether a different byte budget is intended, and what an absent MIME value should communicate. Add fixtures for those choices before presenting a changed result to users. A passing suite is most useful when the surrounding language remains as narrow and clear as the behavior it actually exercises.
Top comments (0)