Skip to content
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
23 changes: 18 additions & 5 deletions apps/server/src/environmentTheme.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
DEFAULT_SERVER_SETTINGS,
EnvironmentThemeFile,
ServerSettingsError,
THEME_CSS_MAX_LENGTH,
} from "@t3tools/contracts";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it } from "@effect/vitest";
Expand All @@ -20,6 +21,12 @@ import { ServerSettingsService } from "./serverSettings.ts";
import { symlinksSupported } from "@t3tools/shared/testing/symlinks";

const encodeThemeFile = Schema.encodeSync(Schema.fromJsonString(EnvironmentThemeFile));
/** Writes non-ASCII characters as `\uXXXX` escapes, as encoders such as Python's json.dumps do. */
const encodeThemeFileEscaped = (theme: EnvironmentThemeFile) =>
encodeThemeFile(theme).replace(
/[\u0080-\uffff]/g,
(char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`,
);

const NIGHTFALL_THEME: EnvironmentThemeFile = {
name: "Nightfall",
Expand Down Expand Up @@ -190,7 +197,10 @@ it.layer(NodeServices.layer)("environment theme", (it) => {
);

it.effect("does not replace a theme file it could not put back", () => {
const oversized = JSON.stringify({ ...NIGHTFALL_THEME, padding: "x".repeat(40_000) });
const oversized = JSON.stringify({
...NIGHTFALL_THEME,
padding: "x".repeat(EnvironmentTheme.MAX_THEME_FILE_BYTES),
});
return withEnvironmentThemes(
{ [`${REFUSED_THEME_ID}.json`]: oversized },
Effect.gen(function* () {
Expand Down Expand Up @@ -298,14 +308,14 @@ it.layer(NodeServices.layer)("environment theme", (it) => {
const large = (name: string): EnvironmentThemeFile => ({
...NIGHTFALL_THEME,
name,
css: `/* ${"x".repeat(19_900)} */`,
css: `/* ${"界".repeat(THEME_CSS_MAX_LENGTH - 6)} */`,
});
return withEnvironmentThemes(
{
...Object.fromEntries(
Array.from({ length: 9 }, (_, index) => [
Array.from({ length: 6 }, (_, index) => [
`aa-${index}.json`,
encodeThemeFile(large(`Large ${index}`)),
encodeThemeFileEscaped(large(`Large ${index}`)),
]),
),
"zz-look.json": encodeThemeFile(NIGHTFALL_THEME),
Expand Down Expand Up @@ -341,7 +351,10 @@ it.layer(NodeServices.layer)("environment theme", (it) => {
withEnvironmentThemes(
{
...Object.fromEntries(
Array.from({ length: 7 }, (_, index) => [`junk-${index}.json`, "{".repeat(30_000)]),
Array.from({ length: 7 }, (_, index) => [
`junk-${index}.json`,
"{".repeat(EnvironmentTheme.MAX_THEME_FILE_BYTES),
]),
),
"zz-valid.json": encodeThemeFile(NIGHTFALL_THEME),
},
Expand Down
10 changes: 7 additions & 3 deletions apps/server/src/environmentTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
EnvironmentThemeFile,
EnvironmentThemeId,
environmentThemeFileHasColors,
THEME_CSS_MAX_LENGTH,
} from "@t3tools/contracts";
import { fromJsonStringPretty } from "@t3tools/shared/schemaJson";
import { themeCssLoadsRemoteFiles } from "@t3tools/shared/themeCss";
Expand Down Expand Up @@ -44,9 +45,12 @@ const isEnvironmentThemeId = Schema.is(EnvironmentThemeId);
const THEME_FILE_SUFFIX = ".json";

const MAX_THEME_FILES = 32;
/** Shared with publishing so it cannot accept files the watcher skips. */
export const MAX_THEME_FILE_BYTES = 32 * 1024;
const MAX_THEME_TOTAL_BYTES = 192 * 1024;
/**
* Shared with publishing so it cannot accept files the watcher skips. Fits the longest CSS in any
* JSON encoding, at up to 6 bytes per character for a `\uXXXX` escape, plus the colors.
*/
export const MAX_THEME_FILE_BYTES = 6 * THEME_CSS_MAX_LENGTH + 16 * 1024;
const MAX_THEME_TOTAL_BYTES = 6 * MAX_THEME_FILE_BYTES;

interface PublishedThemes {
readonly seq: number;
Expand Down
2 changes: 1 addition & 1 deletion packages/contracts/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,7 @@ const EnvironmentThemeColors = Schema.Record(
TrimmedNonEmptyString.check(Schema.isMaxLength(64)),
);

export const THEME_CSS_MAX_LENGTH = 20_000;
export const THEME_CSS_MAX_LENGTH = 50_000;

const environmentThemeFields = {
version: Schema.optional(Schema.Literal(1)),
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/src/themeCss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export const THEME_GUIDE = [
"",
"CSS: `css` applies after the app's styles. Use `.dark` or `:root:not(.dark)` for one appearance.",
"It cannot load files: no @import, no image-set(), and url() only with data: or #id.",
"A background image must be embedded as a data: address. `css` holds 20,000 characters, so keep the image near 10 KB.",
"A background image must be embedded as a data: address. `css` holds 50,000 characters, so keep the image near 30 KB.",
"The app's own rules can be more specific. If a rule has no effect, add !important.",
"It cannot reach the terminal text, the file tree, or diff internals. Use color roles for those.",
"Native scrollbars take `::-webkit-scrollbar` and `::-webkit-scrollbar-thumb`.",
Expand Down
Loading