siyuan-note/siyuan · error

invalid layer source: %w

Error message

invalid layer source: %w

What it means

The layer's src must pass validateBootAppearanceResource for its declared type: safe relative path, no symlink escape, file exists, allowed extension (.png/.jpg/.jpeg/.webp for image, .mp4 for video), size cap, and content sniffing must match the declared type. Failures are wrapped as "invalid layer source".

Source

Thrown at kernel/model/boot_appearance.go:465

		if _, _, err = validateBootAppearanceResource(pluginDir, appearanceDir, manifest.Style, "style"); err != nil {
			return nil, fmt.Errorf("invalid style: %w", err)
		}
		ret.Style = bootAppearanceAssetURL(pkg.Name, appearanceID, manifest.Style)
	}

	layerIDs := map[string]bool{}
	for _, layer := range manifest.Layers {
		if layer == nil || !isValidBootAppearanceID(layer.ID) || layerIDs[layer.ID] {
			err = errors.New("invalid or duplicate layer ID")
			return nil, err
		}
		layerIDs[layer.ID] = true
		if layer.Type != "image" && layer.Type != "video" {
			err = fmt.Errorf("unsupported layer type [%s]", layer.Type)
			return nil, err
		}
		if _, _, err = validateBootAppearanceResource(pluginDir, appearanceDir, layer.Src, layer.Type); err != nil {
			return nil, fmt.Errorf("invalid layer source: %w", err)
		}
		if layer.Type == "video" {
			if layer.Poster == "" {
				err = errors.New("video poster is required")
				return nil, err
			}
			if _, _, err = validateBootAppearanceResource(pluginDir, appearanceDir, layer.Poster, "image"); err != nil {
				return nil, fmt.Errorf("invalid video poster: %w", err)
			}
		} else if layer.Poster != "" {
			err = errors.New("image layer cannot declare a poster")
			return nil, err
		}
		fit := layer.Fit
		if fit == "" {
			fit = "cover"
		}
		if !isValidBootAppearanceFit(fit) {

View on GitHub (pinned to 8641553a1f)

Solutions

  1. Ensure the file referenced by src exists inside the appearance directory and the path is relative with '/' separators
  2. Use an allowed extension and matching real content: .png/.jpg/.jpeg/.webp for image layers, .mp4 (H.264/AAC) for video layers
  3. Re-encode or compress the asset so it is under maxBootAppearanceImageSize / maxBootAppearanceVideoSize
  4. Replace any symlinks in the appearance directory with real files

Example fix

// before
{"id": "anim", "type": "video", "src": "C:\\videos\\anim.webm"}
// after
{"id": "anim", "type": "video", "src": "anim.mp4"}  // real MP4 inside the appearance dir
Defensive patterns

Strategy: validation

Validate before calling

import { existsSync, statSync } from "fs";
manifest.layers?.forEach(l => {
  if (l.src.startsWith("/") || l.src.includes("..") || l.src.includes("\\")) throw new Error(`layer ${l.id}: unsafe src path`);
  const p = join(appearanceDir, l.src);
  if (!existsSync(p) || !statSync(p).isFile()) throw new Error(`layer ${l.id}: src missing: ${l.src}`);
});

Prevention

When it happens

Trigger: GetBootAppearances/getBootAppearanceByID loading a manifest whose layer.src is missing on disk, uses a forbidden path (absolute, '..' traversal, backslashes, leading '/'), has a mismatched extension or actual MIME type (e.g. .mp4 that is really a WebM), or exceeds the size cap.

Common situations: Asset not shipped with the plugin (missing file in the appearance directory); renamed media without updating src; a video renamed to .mp4 without re-encoding (content sniff fails); oversized video or image; path written on Windows with backslashes.

Understand the failure class

Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11). Data as JSON: /api/errors/b2cee5cbc9e8b725. Report an issue: GitHub.