All files / server/services/examples examples-schema.ts

88.09% Statements 37/42
84.37% Branches 27/32
100% Functions 8/8
91.66% Lines 33/36

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116    4x 4x   4x         4x                 4x                                                         3x   3x 3x     3x 3x         3x 3x 3x   3x 5x 5x 2x       3x 1x                 5x 5x 5x 5x   5x     5x           9x 8x 7x 17x 6x       1x           52x   52x        
import { z } from "zod";
 
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
const SAFE_FILE_NAME = /^[A-Z0-9][A-Z0-9_.-]{0,127}\.(?:ino|h)$/i;
 
export const manifestFileSchema = z.object({
  name: z.string().min(1).max(128).regex(SAFE_FILE_NAME),
  path: z.string().min(1).max(512),
});
 
export const manifestExampleSchema = z.object({
  id: z.string().regex(SAFE_ID),
  title: z.string().min(1).max(160),
  category: z.string().min(1).max(100),
  description: z.string().max(500).optional(),
  files: z.array(manifestFileSchema).min(1).max(100),
  main: z.string().regex(SAFE_FILE_NAME),
});
 
export const examplesManifestSchema = z.object({
  schemaVersion: z.literal(1),
  repository: z.string().max(256).optional(),
  ref: z.string().max(128).optional(),
  examples: z.array(manifestExampleSchema).max(1000),
});
 
export type ManifestFile = z.infer<typeof manifestFileSchema>;
export type ManifestExample = z.infer<typeof manifestExampleSchema>;
export type ExamplesManifest = z.infer<typeof examplesManifestSchema>;
 
export type ExampleFile = ManifestFile & {
  content: string;
};
 
export type ExampleRecord = Omit<ManifestExample, "files"> & {
  files: ExampleFile[];
  source: "builtin" | "external";
};
 
export type ExamplesSourceStatus = "builtin" | "remote" | "cache";
 
export type ExamplesSnapshot = {
  status: ExamplesSourceStatus;
  stale: boolean;
  examples: ExampleRecord[];
};
 
export function validateManifestReferences(manifest: ExamplesManifest): void {
  const ids = new Set<string>();
 
  for (const example of manifest.examples) {
    Iif (ids.has(example.id)) {
      throw new Error(`Duplicate example id: ${example.id}`);
    }
    ids.add(example.id);
    validateExampleReferences(example);
  }
}
 
function validateExampleReferences(example: ManifestExample): void {
  const names = new Set<string>();
  const paths = new Set<string>();
  let mainCount = 0;
 
  for (const file of example.files) {
    validateExampleFile(file, names, paths);
    if (file.path.toLowerCase().endsWith(".ino") && file.name === example.main) {
      mainCount++;
    }
  }
 
  if (mainCount !== 1 || !names.has(example.main)) {
    throw new Error(`Invalid main file for example: ${example.id}`);
  }
}
 
function validateExampleFile(
  file: ManifestFile,
  names: Set<string>,
  paths: Set<string>,
): void {
  Iif (names.has(file.name)) throw new Error(`Duplicate file name: ${file.name}`);
  Iif (paths.has(file.path)) throw new Error(`Duplicate file path: ${file.path}`);
  names.add(file.name);
  paths.add(file.path);
 
  Iif (!isSafeRelativePath(file.path)) {
    throw new Error(`Invalid example path: ${file.path}`);
  }
  Iif (file.path.toLowerCase().endsWith(".ino") !== file.name.toLowerCase().endsWith(".ino")) {
    throw new Error(`File name/path extension mismatch: ${file.name}`);
  }
}
 
export function isSafeRelativePath(value: string): boolean {
  if (!value || value.includes("\\") || value.includes("\0")) return false;
  if (value.startsWith("/") || value.startsWith("//")) return false;
  const segments = value.split("/");
  if (segments.some((segment) => !segment || segment === "." || segment === "..")) return false;
  return /\.(?:ino|h)$/i.test(value);
}
 
export function manifestToCatalog(snapshot: ExamplesSnapshot) {
  return {
    schemaVersion: 1 as const,
    source: {
      status: snapshot.status,
      stale: snapshot.stale,
    },
    examples: snapshot.examples.map(({ files, ...example }) => ({
      ...example,
      files: files.map(({ content: _content, ...file }) => file),
    })),
  };
}