Skip to content

Commit

Permalink
fix icon bug, add more tests
Browse files Browse the repository at this point in the history
  • Loading branch information
TurtleP committed Jun 18, 2024
1 parent 678910e commit 0b136d7
Show file tree
Hide file tree
Showing 32 changed files with 1,000 additions and 148 deletions.
14 changes: 7 additions & 7 deletions Bundler.Client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 47 additions & 7 deletions Bundler.Client/src/services/Bundle.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
import JSZip, { JSZipObject } from "jszip";
import Config, { ConfigMetadata, parseConfig } from "./Config";
import Config, { Metadata, loadConfig } from "./Config";

import { BundleIcons, BundleType } from "./types";

import mime from "mime";

const IconMimetypes: Record<string, Array<string>> = {
"ctr": ["image/png"],
"hac": ["image/jpeg", "image/jpg"],
"cafe": ["image/png"]
};

const ExpectedDimensions: Record<string, number> = {
"ctr": 48,
"hac": 256,
"cafe": 128
};

/*
** Bundler class
** Represents a bundle of files and configuration.
Expand All @@ -14,11 +28,29 @@ export default class Bundle {
private config: Config | undefined;

readonly ConfigName = "lovebrew.toml";
private configContent: string | undefined;

constructor(zip: File) {
this.file = zip;
}

private async validateIconDimensions(target: string, file: JSZipObject): Promise<boolean> {
try {
const data = await file.async("blob");

const image = await createImageBitmap(data);
const dimensions = [image.width, image.height];

if (dimensions.some((dim) => dim != ExpectedDimensions[target])) {
return false;
}

return true;
} catch (error) {
throw new Error(`Invalid icon for ${target}.`);
}
}

/**
* Validates the bundle
* @returns {Promise<boolean>} - Whether the file is a valid bundle.
Expand All @@ -28,11 +60,11 @@ export default class Bundle {

const data = await this.zip.file(this.ConfigName)?.async("string");

if (data === undefined) {
throw Error("Missing configuration file.");
}
if (data === undefined) throw Error("Missing configuration file.");
if (data.trim() === "") throw Error("Invalid configuration file.");

this.config = parseConfig(data);
this.configContent = data;
this.config = loadConfig(data);

const source = this.config.build.source;
if (this.zip.file(new RegExp(`^${source}/.+`)).length === 0) {
Expand All @@ -58,11 +90,19 @@ export default class Bundle {
const result: BundleIcons = {};
const icons = this.config.getIcons();

if (icons === undefined) return result;

for (const [key, value] of Object.entries(icons)) {
const file = this.zip.file(value);

if (file === null) continue;

const mimetype = mime.getType(file.name)

if (mimetype === null) throw new Error(`Icon for ${key} has no mimetype.`);
if (!IconMimetypes[key].includes(mimetype)) throw new Error(`Invalid ${key} icon mimetype.`);
if (!await this.validateIconDimensions(key, file)) throw new Error(`Invalid ${key} icon dimensions.`);

const blob = await file.async("blob");
result[key as keyof BundleIcons] = blob;
}
Expand Down Expand Up @@ -133,10 +173,10 @@ export default class Bundle {
iconData += await this.blobToBase64(icons[key] as Blob);
}

return this.config.source + iconData;
return this.configContent + iconData;
}

public getMetadata(): ConfigMetadata {
public getMetadata(): Metadata {
if (this.config === undefined) {
throw Error("Configuration file not loaded.");
}
Expand Down
14 changes: 10 additions & 4 deletions Bundler.Client/src/services/Bundler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Bundle from "./Bundle";
import { ConfigMetadata } from "./Config";
import { Metadata } from "./Config";
import {
BundleIcons,
BundleCache,
Expand All @@ -8,6 +8,7 @@ import {
getExtension,
BundleAssetCache,
MediaFile,
getIconExtension,
} from "./types";

import MediaConverter from "./MediaConverter";
Expand Down Expand Up @@ -283,13 +284,18 @@ export default class Bundler {
private async sendCompile(
targets: Array<BundleType>,
icons: BundleIcons,
metadata: ConfigMetadata
metadata: Metadata
): Promise<BundleCache> {
// append the icons as FormData
const body = new FormData();
for (const [target, blob] of Object.entries(icons)) {
let key: BundleType;

for (key in icons) {
const blob = icons[key];
if (blob === undefined) continue;
body.append(`icon-${target}`, blob as Blob);

const basename = `icon-${key}`;
body.append(basename, blob, `${basename}.${getIconExtension(key)}`);
}

const url = `${import.meta.env.DEV ? process.env.BASE_URL : ""}`;
Expand Down
84 changes: 65 additions & 19 deletions Bundler.Client/src/services/Config.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,55 @@
import toml from "toml";
import { BundleType } from "./types";

export type ConfigIcons = {
ctr?: string;
cafe?: string;
hac?: string;
// Validation
const validateString = (value: any) => {
return (typeof value === "string" || value instanceof String) && value.trim() !== "";
}

const validateBoolean = (value: any) => {
return typeof value === "boolean" || value instanceof Boolean;
}

const validateBundleTypeList = (value: any) => {
return (Array.isArray(value) && value.every((x) => x === "ctr" || x === "hac" || x === "cafe"));
}

type Icons = {
[key in BundleType]?: string;
};

export type ConfigMetadata = {
export type Metadata = {
title: string;
author: string;
description: string;
version: string;
icons: ConfigIcons;
icons?: Icons;
};

const MetadataFields: Record<string, Function> = {
"title": validateString,
"author": validateString,
"description": validateString,
"version": validateString
};

export type ConfigBuild = {
targets: Array<string>;
type Build = {
targets: Array<BundleType>;
source: string;
packaged?: boolean;
};

const BuildFields: Record<string, Function> = {
"targets": validateBundleTypeList,
"source": validateString,
"packaged": validateBoolean
}

export default class Config {
metadata!: ConfigMetadata;
build!: ConfigBuild;
public source: string = "";
metadata!: Metadata;
build!: Build;

public getIcons(): ConfigIcons {
public getIcons(): Icons | undefined {
return this.metadata.icons;
}

Expand All @@ -39,12 +62,35 @@ export default class Config {
}
}

export function parseConfig(content: string): Config {
const configData = toml.parse(content);
export function loadConfig(content: string): Config {
let parsed: undefined;

const config = new Config();
config.source = content;
try {
parsed = toml.parse(content);
} catch (exception) {
throw new Error("Invalid config content. Unable to parse TOML.");
}

Object.assign(config, configData);
return config;
}
if (parsed === undefined) throw new Error("Invalid config content. Unable to parse TOML.");

if (parsed["metadata"] == null || parsed["build"] == null) {
const missing = parsed["metadata"] == null ? "metadata" : "build";
throw new Error(`Invalid config content. Missing section: '${missing}'.`);
}

for (const field in MetadataFields) {
if (parsed["metadata"][field] == null) throw new Error(`Missing config 'metadata' field '${field}'.`);

const value = parsed["metadata"][field];
if (!MetadataFields[field](value)) throw new Error(`Config 'metadata' field '${field}' type is invalid.`);
}

for (const field in BuildFields) {
if (parsed["build"][field] == null) throw new Error(`Missing config 'build' field '${field}'.`);

const value = parsed["build"][field];
if (!BuildFields[field](value)) throw new Error(`Config 'build' field '${field}' type is invalid.`);
}

return Object.assign(new Config(), parsed);
}
2 changes: 1 addition & 1 deletion Bundler.Client/src/services/MediaConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ export default class MediaConverter {

for (const file of fileMap) {
if (!(await MediaConverter.validateFile(file))) {
throw Error(`Invalid file: ${file.filepath}`);
throw Error(`Invalid file: ${file.filepath}.`);
}
body.append(file.filepath, file.data);
}
Expand Down
8 changes: 8 additions & 0 deletions Bundler.Client/src/services/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@ const extMap: Record<BundleType, BundleExtension> = {
cafe: "wuhb",
hac: "nro",
};

const iconMap: Record<BundleType, string> = {
ctr: "png",
cafe: "png",
hac: "jpg",
}

export const getExtension = (type: BundleType): BundleExtension => extMap[type];
export const getIconExtension = (type: BundleType): string => iconMap[type];

export type BundleIcons = {
[key in BundleType]?: Blob;
Expand Down
Loading

0 comments on commit 0b136d7

Please sign in to comment.