All files / shared input-limits.ts

100% Statements 17/17
100% Branches 13/13
100% Functions 3/3
100% Lines 16/16

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              31x                                                                 31x         31x         64x       34x   30x 30x 3x   27x       19x         2x   17x     31x                                                   28x 27x 28x    
/**
 * Authoritative limits for data crossing the REST and WebSocket boundary.
 *
 * Keep this module dependency-free so client and server schemas share one
 * contract. Values are intentionally expressed in bytes where payload size is
 * relevant; JavaScript string limits are measured in UTF-16 code units by Zod.
 */
export const INPUT_LIMITS = {
  rest: {
    maxBodyBytes: 1024 * 1024,
  },
  compile: {
    maxCodeChars: 128 * 1024,
    maxHeaders: 20,
    maxHeaderNameChars: 128,
    maxHeaderContentChars: 32 * 1024,
    maxLibraries: 20,
    maxLibraryNameChars: 128,
    maxFqbnChars: 128,
  },
  webSocket: {
    maxPayloadBytes: 256 * 1024,
    maxSerialInputChars: 4 * 1024,
    maxTestRunIdChars: 64,
  },
  simulation: {
    minTimeoutSeconds: 1,
    defaultTimeoutSeconds: 60,
    maxTimeoutSeconds: 300,
    minPin: 0,
    maxPin: 19,
    // Arduino UNO ADC readings use the full 10-bit range. Digital writes use
    // only 0/1, but this shared WebSocket command also carries analog inputs.
    maxPinValue: 1023,
    minBaudrate: 300,
    maxBaudrate: 115_200,
  },
} as const;
 
/** URL-safe identifier accepted for test-only artifact namespaces. */
export const TEST_RUN_ID_PATTERN = new RegExp(
  `^[A-Za-z0-9_-]{1,${INPUT_LIMITS.webSocket.maxTestRunIdChars}}$`,
);
 
/** Portable header basename; separators and traversal tokens cannot match. */
export const HEADER_NAME_PATTERN = new RegExp(
  `^[A-Za-z0-9][A-Za-z0-9_.-]{0,${INPUT_LIMITS.compile.maxHeaderNameChars - 1}}$`,
);
 
export function normalizeSimulationTimeout(timeoutSeconds: number | undefined): number {
  if (
    timeoutSeconds === undefined ||
    !Number.isFinite(timeoutSeconds)
  ) {
    return INPUT_LIMITS.simulation.defaultTimeoutSeconds;
  }
  const roundedTimeout = Math.floor(timeoutSeconds);
  if (roundedTimeout < INPUT_LIMITS.simulation.minTimeoutSeconds) {
    return INPUT_LIMITS.simulation.defaultTimeoutSeconds;
  }
  return Math.min(roundedTimeout, INPUT_LIMITS.simulation.maxTimeoutSeconds);
}
 
export function normalizeBaudrate(baudrate: number): number {
  if (
    !Number.isInteger(baudrate) ||
    baudrate < INPUT_LIMITS.simulation.minBaudrate ||
    baudrate > INPUT_LIMITS.simulation.maxBaudrate
  ) {
    return 9600;
  }
  return baudrate;
}
 
const WINDOWS_RESERVED_BASENAMES = new Set([
  "CON",
  "PRN",
  "AUX",
  "NUL",
  "COM1",
  "COM2",
  "COM3",
  "COM4",
  "COM5",
  "COM6",
  "COM7",
  "COM8",
  "COM9",
  "LPT1",
  "LPT2",
  "LPT3",
  "LPT4",
  "LPT5",
  "LPT6",
  "LPT7",
  "LPT8",
  "LPT9",
]);
 
export function isSafeHeaderName(name: string): boolean {
  if (!HEADER_NAME_PATTERN.test(name)) return false;
  const basename = name.split(".")[0]?.toUpperCase();
  return !WINDOWS_RESERVED_BASENAMES.has(basename);
}