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

87.17% Statements 34/39
84.37% Branches 27/32
100% Functions 5/5
90.9% Lines 30/33

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      13x 13x   13x         13x                 13x                                             31x   31x 38x     38x 38x         38x 38x 38x   38x 43x 43x 36x       38x 2x                 43x 43x 43x 43x   43x     43x           47x 46x 45x 91x 44x    
import { z } from "zod";
import type { ExampleTutorAnnotation } from "../course-content/embedded-tutor-annotation";
 
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 ExamplesManifestCore = Omit<ExamplesManifest, "schemaVersion"> & { schemaVersion: 1 | 2 };
 
export type ExampleFile = ManifestFile & {
  content: string;
};
 
export type ExampleRecord = Omit<ManifestExample, "files"> & {
  files: ExampleFile[];
  source: "builtin" | "external";
  tutorAnnotation?: ExampleTutorAnnotation;
};
 
export function validateManifestReferences(manifest: Pick<ExamplesManifest, "examples">): 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);
}