@emasoft/svg-matrix 1.0.30 → 1.0.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/bin/svg-matrix.js +310 -61
  2. package/bin/svglinter.cjs +102 -3
  3. package/bin/svgm.js +236 -27
  4. package/package.json +1 -1
  5. package/src/animation-optimization.js +137 -17
  6. package/src/animation-references.js +123 -6
  7. package/src/arc-length.js +213 -4
  8. package/src/bezier-analysis.js +217 -21
  9. package/src/bezier-intersections.js +275 -12
  10. package/src/browser-verify.js +237 -4
  11. package/src/clip-path-resolver.js +168 -0
  12. package/src/convert-path-data.js +479 -28
  13. package/src/css-specificity.js +73 -10
  14. package/src/douglas-peucker.js +219 -2
  15. package/src/flatten-pipeline.js +284 -26
  16. package/src/geometry-to-path.js +250 -25
  17. package/src/gjk-collision.js +236 -33
  18. package/src/index.js +261 -3
  19. package/src/inkscape-support.js +86 -28
  20. package/src/logger.js +48 -3
  21. package/src/marker-resolver.js +278 -74
  22. package/src/mask-resolver.js +265 -66
  23. package/src/matrix.js +44 -5
  24. package/src/mesh-gradient.js +352 -102
  25. package/src/off-canvas-detection.js +382 -13
  26. package/src/path-analysis.js +192 -18
  27. package/src/path-data-plugins.js +309 -5
  28. package/src/path-optimization.js +129 -5
  29. package/src/path-simplification.js +188 -32
  30. package/src/pattern-resolver.js +454 -106
  31. package/src/polygon-clip.js +324 -1
  32. package/src/svg-boolean-ops.js +226 -9
  33. package/src/svg-collections.js +7 -5
  34. package/src/svg-flatten.js +386 -62
  35. package/src/svg-parser.js +179 -8
  36. package/src/svg-rendering-context.js +235 -6
  37. package/src/svg-toolbox.js +45 -8
  38. package/src/svg2-polyfills.js +40 -10
  39. package/src/transform-decomposition.js +258 -32
  40. package/src/transform-optimization.js +259 -13
  41. package/src/transforms2d.js +82 -9
  42. package/src/transforms3d.js +62 -10
  43. package/src/use-symbol-resolver.js +286 -42
  44. package/src/vector.js +64 -8
  45. package/src/verification.js +392 -1
package/bin/svg-matrix.js CHANGED
@@ -237,15 +237,22 @@ process.on("SIGTERM", () => handleGracefulExit("SIGTERM")); // kill command
237
237
  * @returns {void}
238
238
  */
239
239
  function writeToLogFile(message) {
240
- if (config.logFile) {
241
- try {
242
- const timestamp = new Date().toISOString();
243
- // eslint-disable-next-line no-control-regex -- ANSI escape codes are intentional for log stripping
244
- const cleanMessage = message.replace(/\x1b\[[0-9;]*m/g, "");
245
- appendFileSync(config.logFile, `[${timestamp}] ${cleanMessage}\n`);
246
- } catch {
247
- /* ignore */
240
+ // Why: Validate message parameter to prevent crashes
241
+ if (!message || !config.logFile) return;
242
+
243
+ try {
244
+ // Why: Ensure log directory exists before writing
245
+ const logDir = dirname(config.logFile);
246
+ if (!existsSync(logDir)) {
247
+ mkdirSync(logDir, { recursive: true });
248
248
  }
249
+
250
+ const timestamp = new Date().toISOString();
251
+ // eslint-disable-next-line no-control-regex -- ANSI escape codes are intentional for log stripping
252
+ const cleanMessage = message.replace(/\x1b\[[0-9;]*m/g, "");
253
+ appendFileSync(config.logFile, `[${timestamp}] ${cleanMessage}\n`);
254
+ } catch {
255
+ /* ignore */
249
256
  }
250
257
  }
251
258
 
@@ -319,6 +326,10 @@ function showProgress(current, total, filename) {
319
326
  // Why: Don't show progress in quiet mode or when there's only one file
320
327
  if (config.quiet || total <= 1) return;
321
328
 
329
+ // Why: Validate parameters to prevent crashes
330
+ if (!filename || typeof current !== "number" || typeof total !== "number")
331
+ return;
332
+
322
333
  // Why: In verbose mode, newline before progress to avoid overwriting debug output
323
334
  if (config.verbose && current > 1) {
324
335
  process.stdout.write("\n");
@@ -355,6 +366,16 @@ function showProgress(current, total, filename) {
355
366
  * @throws {Error} If file is invalid or too large
356
367
  */
357
368
  function validateSvgFile(filePath) {
369
+ // Why: Validate parameter to prevent crashes
370
+ if (!filePath || typeof filePath !== "string") {
371
+ throw new Error("Invalid file path: must be a non-empty string");
372
+ }
373
+
374
+ // Why: Check file exists before attempting to stat
375
+ if (!existsSync(filePath)) {
376
+ throw new Error(`File not found: ${filePath}`);
377
+ }
378
+
358
379
  const stats = statSync(filePath);
359
380
 
360
381
  // Why: Prevent memory exhaustion from huge files
@@ -365,15 +386,27 @@ function validateSvgFile(filePath) {
365
386
  }
366
387
 
367
388
  // Why: Read only first 1KB to check header - don't load entire file
368
- const fd = openSync(filePath, "r");
369
- const buffer = Buffer.alloc(1024);
370
- readSync(fd, buffer, 0, 1024, 0);
371
- closeSync(fd);
372
- const header = buffer.toString("utf8");
373
-
374
- // Why: SVG files must have an <svg> element - if not, it's not a valid SVG
375
- if (!CONSTANTS.SVG_HEADER_PATTERN.test(header)) {
376
- throw new Error("Not a valid SVG file (missing <svg> element)");
389
+ // Use try-finally to ensure fd is always closed, preventing file descriptor leak
390
+ let fd;
391
+ try {
392
+ fd = openSync(filePath, "r");
393
+ const buffer = Buffer.alloc(1024);
394
+ readSync(fd, buffer, 0, 1024, 0);
395
+ const header = buffer.toString("utf8");
396
+
397
+ // Why: SVG files must have an <svg> element - if not, it's not a valid SVG
398
+ if (!CONSTANTS.SVG_HEADER_PATTERN.test(header)) {
399
+ throw new Error("Not a valid SVG file (missing <svg> element)");
400
+ }
401
+ } finally {
402
+ // Why: Always close file descriptor to prevent resource leak
403
+ if (fd !== undefined) {
404
+ try {
405
+ closeSync(fd);
406
+ } catch {
407
+ /* ignore close errors */
408
+ }
409
+ }
377
410
  }
378
411
 
379
412
  return true;
@@ -383,34 +416,9 @@ function validateSvgFile(filePath) {
383
416
  // WRITE VERIFICATION
384
417
  // Why: Detect silent write failures. Some filesystems (especially network
385
418
  // shares) may appear to write successfully but fail to persist data.
386
- // Verification catches this immediately rather than discovering corruption later.
419
+ // Verification is done inline where needed (see processFlatten/Convert/Normalize)
420
+ // rather than in a dedicated function to avoid double memory usage.
387
421
  // ============================================================================
388
- /**
389
- * Verify file was written correctly by comparing content.
390
- * @param {string} filePath - Path to file
391
- * @param {string} expectedContent - Expected file content
392
- * @returns {boolean} True if verification passed
393
- * @throws {Error} If verification failed
394
- * @private
395
- */
396
- function _verifyWriteSuccess(filePath, expectedContent) {
397
- // Why: Read back what was written and compare
398
- const actualContent = readFileSync(filePath, "utf8");
399
-
400
- // Why: Compare lengths first (fast), then content if needed
401
- if (actualContent.length !== expectedContent.length) {
402
- throw new Error(
403
- `Write verification failed: size mismatch (expected ${expectedContent.length}, got ${actualContent.length})`,
404
- );
405
- }
406
-
407
- // Why: Full content comparison to catch bit flips or encoding issues
408
- if (actualContent !== expectedContent) {
409
- throw new Error("Write verification failed: content mismatch");
410
- }
411
-
412
- return true;
413
- }
414
422
 
415
423
  // ============================================================================
416
424
  // CRASH LOG
@@ -473,6 +481,8 @@ ${JSON.stringify({ ...config, logFile: config.logFile ? "[redacted]" : null }, n
473
481
  * @returns {string} Normalized path
474
482
  */
475
483
  function normalizePath(p) {
484
+ // Why: Validate parameter to prevent crashes
485
+ if (!p || typeof p !== "string") return "";
476
486
  return p.replace(/\\/g, "/");
477
487
  }
478
488
 
@@ -482,6 +492,10 @@ function normalizePath(p) {
482
492
  * @returns {string} Absolute normalized path
483
493
  */
484
494
  function resolvePath(p) {
495
+ // Why: Validate parameter to prevent crashes
496
+ if (!p || typeof p !== "string") {
497
+ throw new Error("Invalid path: must be a non-empty string");
498
+ }
485
499
  return isAbsolute(p)
486
500
  ? normalizePath(p)
487
501
  : normalizePath(resolve(process.cwd(), p));
@@ -493,6 +507,8 @@ function resolvePath(p) {
493
507
  * @returns {boolean} True if directory exists
494
508
  */
495
509
  function isDir(p) {
510
+ // Why: Validate parameter to prevent crashes from invalid types
511
+ if (!p || typeof p !== "string") return false;
496
512
  try {
497
513
  return statSync(p).isDirectory();
498
514
  } catch {
@@ -506,6 +522,8 @@ function isDir(p) {
506
522
  * @returns {boolean} True if file exists
507
523
  */
508
524
  function isFile(p) {
525
+ // Why: Validate parameter to prevent crashes from invalid types
526
+ if (!p || typeof p !== "string") return false;
509
527
  try {
510
528
  return statSync(p).isFile();
511
529
  } catch {
@@ -519,6 +537,10 @@ function isFile(p) {
519
537
  * @returns {void}
520
538
  */
521
539
  function ensureDir(dir) {
540
+ // Why: Validate parameter to prevent crashes
541
+ if (!dir || typeof dir !== "string") {
542
+ throw new Error("Invalid directory path: must be a non-empty string");
543
+ }
522
544
  if (!existsSync(dir)) {
523
545
  mkdirSync(dir, { recursive: true });
524
546
  logDebug(`Created directory: ${dir}`);
@@ -532,6 +554,14 @@ function ensureDir(dir) {
532
554
  * @returns {string[]} Array of SVG file paths
533
555
  */
534
556
  function getSvgFiles(dir, recursive = false) {
557
+ // Why: Validate parameter to prevent crashes
558
+ if (!dir || typeof dir !== "string") {
559
+ throw new Error("Invalid directory path: must be a non-empty string");
560
+ }
561
+ if (!existsSync(dir)) {
562
+ throw new Error(`Directory not found: ${dir}`);
563
+ }
564
+
535
565
  const files = [];
536
566
  function scan(d) {
537
567
  for (const entry of readdirSync(d, { withFileTypes: true })) {
@@ -555,16 +585,28 @@ function getSvgFiles(dir, recursive = false) {
555
585
  * @returns {string[]} Array of resolved file paths
556
586
  */
557
587
  function parseFileList(listPath) {
588
+ // Why: Validate parameter to prevent crashes
589
+ if (!listPath || typeof listPath !== "string") {
590
+ throw new Error("Invalid list file path: must be a non-empty string");
591
+ }
592
+ if (!existsSync(listPath)) {
593
+ throw new Error(`List file not found: ${listPath}`);
594
+ }
595
+
558
596
  const content = readFileSync(listPath, "utf8");
559
597
  const files = [];
560
598
  for (const line of content.split(/\r?\n/)) {
561
599
  const trimmed = line.trim();
562
600
  if (!trimmed || trimmed.startsWith("#")) continue;
563
- const resolved = resolvePath(trimmed);
564
- if (isFile(resolved)) files.push(resolved);
565
- else if (isDir(resolved))
566
- files.push(...getSvgFiles(resolved, config.recursive));
567
- else logWarn(`File not found: ${trimmed}`);
601
+ try {
602
+ const resolved = resolvePath(trimmed);
603
+ if (isFile(resolved)) files.push(resolved);
604
+ else if (isDir(resolved))
605
+ files.push(...getSvgFiles(resolved, config.recursive));
606
+ else logWarn(`File not found: ${trimmed}`);
607
+ } catch (e) {
608
+ logWarn(`Invalid path in list: ${trimmed} - ${e.message}`);
609
+ }
568
610
  }
569
611
  return files;
570
612
  }
@@ -584,10 +626,17 @@ function parseFileList(listPath) {
584
626
  * @returns {number} Parsed value or default
585
627
  */
586
628
  function extractNumericAttr(attrs, attrName, defaultValue = 0) {
629
+ // Why: Validate parameters to prevent crashes
630
+ if (!attrs || typeof attrs !== "string") return defaultValue;
631
+ if (!attrName || typeof attrName !== "string") return defaultValue;
632
+
587
633
  // Why: Use word boundary \b to avoid matching 'rx' when looking for 'x'
588
634
  const regex = new RegExp(`\\b${attrName}\\s*=\\s*["']([^"']+)["']`, "i");
589
635
  const match = attrs.match(regex);
590
- return match ? parseFloat(match[1]) : defaultValue;
636
+ if (!match) return defaultValue;
637
+ const val = parseFloat(match[1]);
638
+ // Why: Return default if parsing fails or results in NaN/Infinity
639
+ return isNaN(val) || !isFinite(val) ? defaultValue : val;
591
640
  }
592
641
 
593
642
  /**
@@ -598,6 +647,18 @@ function extractNumericAttr(attrs, attrName, defaultValue = 0) {
598
647
  * @returns {string|null} Path data or null if extraction failed
599
648
  */
600
649
  function extractShapeAsPath(shapeType, attrs, precision) {
650
+ // Why: Validate parameters to prevent crashes
651
+ if (!shapeType || typeof shapeType !== "string") return null;
652
+ if (!attrs || typeof attrs !== "string") return null;
653
+ // Why: Validate precision is a valid finite number within acceptable range
654
+ if (
655
+ typeof precision !== "number" ||
656
+ !isFinite(precision) ||
657
+ precision < 1 ||
658
+ precision > CONSTANTS.MAX_PRECISION
659
+ )
660
+ return null;
661
+
601
662
  switch (shapeType) {
602
663
  case "rect": {
603
664
  const x = extractNumericAttr(attrs, "x");
@@ -606,7 +667,8 @@ function extractShapeAsPath(shapeType, attrs, precision) {
606
667
  const h = extractNumericAttr(attrs, "height");
607
668
  const rx = extractNumericAttr(attrs, "rx");
608
669
  const ry = extractNumericAttr(attrs, "ry", rx); // ry defaults to rx per SVG spec
609
- if (w <= 0 || h <= 0) return null;
670
+ // Why: Validate dimensions and corner radii are positive and finite
671
+ if (w <= 0 || h <= 0 || rx < 0 || ry < 0) return null;
610
672
  return GeometryToPath.rectToPathData(
611
673
  x,
612
674
  y,
@@ -622,6 +684,7 @@ function extractShapeAsPath(shapeType, attrs, precision) {
622
684
  const cx = extractNumericAttr(attrs, "cx");
623
685
  const cy = extractNumericAttr(attrs, "cy");
624
686
  const r = extractNumericAttr(attrs, "r");
687
+ // Why: Validate radius is positive (already checked by extractNumericAttr for NaN/Infinity)
625
688
  if (r <= 0) return null;
626
689
  return GeometryToPath.circleToPathData(cx, cy, r, precision);
627
690
  }
@@ -630,6 +693,7 @@ function extractShapeAsPath(shapeType, attrs, precision) {
630
693
  const cy = extractNumericAttr(attrs, "cy");
631
694
  const rx = extractNumericAttr(attrs, "rx");
632
695
  const ry = extractNumericAttr(attrs, "ry");
696
+ // Why: Validate radii are positive (already checked by extractNumericAttr for NaN/Infinity)
633
697
  if (rx <= 0 || ry <= 0) return null;
634
698
  return GeometryToPath.ellipseToPathData(cx, cy, rx, ry, precision);
635
699
  }
@@ -663,6 +727,9 @@ function extractShapeAsPath(shapeType, attrs, precision) {
663
727
  * @returns {string[]} Attribute names to remove
664
728
  */
665
729
  function getShapeSpecificAttrs(shapeType) {
730
+ // Why: Validate parameter and return empty array for invalid input
731
+ if (!shapeType || typeof shapeType !== "string") return [];
732
+
666
733
  const attrMap = {
667
734
  rect: ["x", "y", "width", "height", "rx", "ry"],
668
735
  circle: ["cx", "cy", "r"],
@@ -681,8 +748,13 @@ function getShapeSpecificAttrs(shapeType) {
681
748
  * @returns {string} Cleaned attributes
682
749
  */
683
750
  function removeShapeAttrs(attrs, attrsToRemove) {
751
+ // Why: Validate parameters to prevent crashes
752
+ if (!attrs || typeof attrs !== "string") return "";
753
+ if (!Array.isArray(attrsToRemove)) return attrs;
754
+
684
755
  let result = attrs;
685
756
  for (const attrName of attrsToRemove) {
757
+ if (!attrName || typeof attrName !== "string") continue;
686
758
  result = result.replace(
687
759
  new RegExp(`\\b${attrName}\\s*=\\s*["'][^"']*["']`, "gi"),
688
760
  "",
@@ -726,6 +798,8 @@ const B = supportsUnicode()
726
798
  * @returns {string} String without ANSI codes
727
799
  */
728
800
  function stripAnsi(s) {
801
+ // Why: Validate parameter to prevent crashes
802
+ if (!s || typeof s !== "string") return "";
729
803
  // eslint-disable-next-line no-control-regex -- ANSI escape codes are intentional for terminal color stripping
730
804
  return s.replace(/\x1b\[[0-9;]*m/g, "");
731
805
  }
@@ -737,9 +811,14 @@ function stripAnsi(s) {
737
811
  * @returns {string} Formatted line
738
812
  */
739
813
  function boxLine(content, width = 70) {
740
- const visible = stripAnsi(content).length;
814
+ // Why: Validate content parameter to prevent crashes
815
+ let safeContent = content;
816
+ if (safeContent === undefined || safeContent === null) safeContent = "";
817
+ if (typeof safeContent !== "string") safeContent = String(safeContent);
818
+
819
+ const visible = stripAnsi(safeContent).length;
741
820
  const padding = Math.max(0, width - visible - 2);
742
- return `${colors.cyan}${B.v}${colors.reset} ${content}${" ".repeat(padding)}${colors.cyan}${B.v}${colors.reset}`;
821
+ return `${colors.cyan}${B.v}${colors.reset} ${safeContent}${" ".repeat(padding)}${colors.cyan}${B.v}${colors.reset}`;
743
822
  }
744
823
 
745
824
  /**
@@ -749,10 +828,14 @@ function boxLine(content, width = 70) {
749
828
  * @returns {string} Formatted header
750
829
  */
751
830
  function boxHeader(title, width = 70) {
752
- const _hr = B.h.repeat(width); // Reserved for future use (horizontal rule)
753
- const visible = stripAnsi(title).length;
831
+ // Why: Validate title parameter to prevent crashes
832
+ let safeTitle = title;
833
+ if (safeTitle === undefined || safeTitle === null) safeTitle = "";
834
+ if (typeof safeTitle !== "string") safeTitle = String(safeTitle);
835
+
836
+ const visible = stripAnsi(safeTitle).length;
754
837
  const padding = Math.max(0, width - visible - 2);
755
- return `${colors.cyan}${B.v}${colors.reset} ${colors.bright}${title}${colors.reset}${" ".repeat(padding)}${colors.cyan}${B.v}${colors.reset}`;
838
+ return `${colors.cyan}${B.v}${colors.reset} ${colors.bright}${safeTitle}${colors.reset}${" ".repeat(padding)}${colors.cyan}${B.v}${colors.reset}`;
756
839
  }
757
840
 
758
841
  /**
@@ -886,6 +969,12 @@ ${colors.cyan}${B.bl}${hr}${B.br}${colors.reset}
886
969
  * @returns {void}
887
970
  */
888
971
  function showCommandHelp(command) {
972
+ // Why: Validate command parameter to prevent crashes
973
+ if (!command || typeof command !== "string") {
974
+ showHelp();
975
+ return;
976
+ }
977
+
889
978
  const W = 72;
890
979
  const hr = B.h.repeat(W);
891
980
 
@@ -1034,6 +1123,8 @@ function showVersion() {
1034
1123
  * @returns {string|null} Transform value or null
1035
1124
  */
1036
1125
  function extractTransform(attrs) {
1126
+ // Why: Validate parameter to prevent crashes
1127
+ if (!attrs || typeof attrs !== "string") return null;
1037
1128
  const match = attrs.match(/transform\s*=\s*["']([^"']+)["']/i);
1038
1129
  return match ? match[1] : null;
1039
1130
  }
@@ -1044,6 +1135,8 @@ function extractTransform(attrs) {
1044
1135
  * @returns {string} Attributes without transform
1045
1136
  */
1046
1137
  function removeTransform(attrs) {
1138
+ // Why: Validate parameter to prevent crashes
1139
+ if (!attrs || typeof attrs !== "string") return "";
1047
1140
  return attrs.replace(/\s*transform\s*=\s*["'][^"']*["']/gi, "");
1048
1141
  }
1049
1142
 
@@ -1053,6 +1146,8 @@ function removeTransform(attrs) {
1053
1146
  * @returns {string|null} Path data or null
1054
1147
  */
1055
1148
  function extractPathD(attrs) {
1149
+ // Why: Validate parameter to prevent crashes
1150
+ if (!attrs || typeof attrs !== "string") return null;
1056
1151
  const match = attrs.match(/\bd\s*=\s*["']([^"']+)["']/i);
1057
1152
  return match ? match[1] : null;
1058
1153
  }
@@ -1064,7 +1159,12 @@ function extractPathD(attrs) {
1064
1159
  * @returns {string} Updated attributes
1065
1160
  */
1066
1161
  function replacePathD(attrs, newD) {
1067
- return attrs.replace(/(\bd\s*=\s*["'])[^"']+["']/i, `$1${newD}"`);
1162
+ // Why: Validate parameters to prevent crashes
1163
+ if (!attrs || typeof attrs !== "string") return "";
1164
+ let safeD = newD;
1165
+ if (safeD === undefined || safeD === null) safeD = "";
1166
+ if (typeof safeD !== "string") safeD = String(safeD);
1167
+ return attrs.replace(/(\bd\s*=\s*["'])[^"']+["']/i, `$1${safeD}"`);
1068
1168
  }
1069
1169
 
1070
1170
  /**
@@ -1086,6 +1186,16 @@ function replacePathD(attrs, newD) {
1086
1186
  * @returns {boolean} True if successful
1087
1187
  */
1088
1188
  function processFlatten(inputPath, outputPath) {
1189
+ // Why: Validate parameters to prevent crashes
1190
+ if (!inputPath || typeof inputPath !== "string") {
1191
+ logError("Invalid input path: must be a non-empty string");
1192
+ return false;
1193
+ }
1194
+ if (!outputPath || typeof outputPath !== "string") {
1195
+ logError("Invalid output path: must be a non-empty string");
1196
+ return false;
1197
+ }
1198
+
1089
1199
  try {
1090
1200
  logDebug(`Processing: ${inputPath}`);
1091
1201
  const svgContent = readFileSync(inputPath, "utf8");
@@ -1116,10 +1226,20 @@ function processFlatten(inputPath, outputPath) {
1116
1226
  };
1117
1227
 
1118
1228
  // Run the full flatten pipeline
1119
- const { svg: flattenedSvg, stats } = FlattenPipeline.flattenSVG(
1120
- svgContent,
1121
- pipelineOptions,
1122
- );
1229
+ const result = FlattenPipeline.flattenSVG(svgContent, pipelineOptions);
1230
+
1231
+ // Why: Validate result structure to prevent crashes on unexpected return values
1232
+ if (!result || typeof result !== "object") {
1233
+ throw new Error("FlattenPipeline.flattenSVG returned invalid result");
1234
+ }
1235
+ if (!result.svg || typeof result.svg !== "string") {
1236
+ throw new Error("FlattenPipeline.flattenSVG returned invalid SVG");
1237
+ }
1238
+ if (!result.stats || typeof result.stats !== "object") {
1239
+ throw new Error("FlattenPipeline.flattenSVG returned invalid stats");
1240
+ }
1241
+
1242
+ const { svg: flattenedSvg, stats } = result;
1123
1243
 
1124
1244
  // Report statistics
1125
1245
  const parts = [];
@@ -1232,6 +1352,20 @@ function processFlatten(inputPath, outputPath) {
1232
1352
  * @private
1233
1353
  */
1234
1354
  function processFlattenLegacy(inputPath, outputPath, svgContent) {
1355
+ // Why: Validate parameters to prevent crashes
1356
+ if (!inputPath || typeof inputPath !== "string") {
1357
+ logError("Invalid input path: must be a non-empty string");
1358
+ return false;
1359
+ }
1360
+ if (!outputPath || typeof outputPath !== "string") {
1361
+ logError("Invalid output path: must be a non-empty string");
1362
+ return false;
1363
+ }
1364
+ if (!svgContent || typeof svgContent !== "string") {
1365
+ logError("Invalid SVG content: must be a non-empty string");
1366
+ return false;
1367
+ }
1368
+
1235
1369
  let result = svgContent;
1236
1370
  let transformCount = 0;
1237
1371
  let pathCount = 0;
@@ -1377,6 +1511,13 @@ function processFlattenLegacy(inputPath, outputPath, svgContent) {
1377
1511
  groupIterations++;
1378
1512
  }
1379
1513
 
1514
+ // Why: Warn when iteration limit reached - may indicate deeply nested transforms
1515
+ if (groupIterations >= CONSTANTS.MAX_GROUP_ITERATIONS) {
1516
+ logWarn(
1517
+ `Group transform propagation reached maximum iterations (${CONSTANTS.MAX_GROUP_ITERATIONS}). Deeply nested groups may not be fully flattened.`,
1518
+ );
1519
+ }
1520
+
1380
1521
  logInfo(
1381
1522
  `Flattened ${transformCount} transforms (${pathCount} paths, ${shapeCount} shapes) [legacy mode]`,
1382
1523
  );
@@ -1396,6 +1537,16 @@ function processFlattenLegacy(inputPath, outputPath, svgContent) {
1396
1537
  * @returns {boolean} True if successful
1397
1538
  */
1398
1539
  function processConvert(inputPath, outputPath) {
1540
+ // Why: Validate parameters to prevent crashes
1541
+ if (!inputPath || typeof inputPath !== "string") {
1542
+ logError("Invalid input path: must be a non-empty string");
1543
+ return false;
1544
+ }
1545
+ if (!outputPath || typeof outputPath !== "string") {
1546
+ logError("Invalid output path: must be a non-empty string");
1547
+ return false;
1548
+ }
1549
+
1399
1550
  try {
1400
1551
  logDebug(`Converting: ${inputPath}`);
1401
1552
  let result = readFileSync(inputPath, "utf8");
@@ -1457,6 +1608,16 @@ function processConvert(inputPath, outputPath) {
1457
1608
  * @returns {boolean} True if successful
1458
1609
  */
1459
1610
  function processNormalize(inputPath, outputPath) {
1611
+ // Why: Validate parameters to prevent crashes
1612
+ if (!inputPath || typeof inputPath !== "string") {
1613
+ logError("Invalid input path: must be a non-empty string");
1614
+ return false;
1615
+ }
1616
+ if (!outputPath || typeof outputPath !== "string") {
1617
+ logError("Invalid output path: must be a non-empty string");
1618
+ return false;
1619
+ }
1620
+
1460
1621
  try {
1461
1622
  logDebug(`Normalizing: ${inputPath}`);
1462
1623
  let result = readFileSync(inputPath, "utf8");
@@ -1488,6 +1649,12 @@ function processNormalize(inputPath, outputPath) {
1488
1649
  * @returns {boolean} True if successful
1489
1650
  */
1490
1651
  function processInfo(inputPath) {
1652
+ // Why: Validate parameter to prevent crashes
1653
+ if (!inputPath || typeof inputPath !== "string") {
1654
+ logError("Invalid input path: must be a non-empty string");
1655
+ return false;
1656
+ }
1657
+
1491
1658
  try {
1492
1659
  const svg = readFileSync(inputPath, "utf8");
1493
1660
  const vb = svg.match(/viewBox\s*=\s*["']([^"']+)["']/i)?.[1] || "not set";
@@ -1623,6 +1790,20 @@ async function testToolboxFunction(
1623
1790
  originalSize,
1624
1791
  outputDir,
1625
1792
  ) {
1793
+ // Why: Validate parameters to prevent crashes
1794
+ if (!fnName || typeof fnName !== "string") {
1795
+ return {
1796
+ name: "invalid",
1797
+ status: "error",
1798
+ error: "Invalid function name",
1799
+ outputSize: 0,
1800
+ sizeDiff: 0,
1801
+ sizeDiffPercent: 0,
1802
+ outputFile: null,
1803
+ timeMs: 0,
1804
+ };
1805
+ }
1806
+
1626
1807
  const result = {
1627
1808
  name: fnName,
1628
1809
  status: "unknown",
@@ -1634,6 +1815,23 @@ async function testToolboxFunction(
1634
1815
  timeMs: 0,
1635
1816
  };
1636
1817
 
1818
+ // Why: Validate remaining parameters after result initialization
1819
+ if (!originalContent || typeof originalContent !== "string") {
1820
+ result.status = "error";
1821
+ result.error = "Invalid original content";
1822
+ return result;
1823
+ }
1824
+ if (typeof originalSize !== "number" || originalSize < 0) {
1825
+ result.status = "error";
1826
+ result.error = "Invalid original size";
1827
+ return result;
1828
+ }
1829
+ if (!outputDir || typeof outputDir !== "string") {
1830
+ result.status = "error";
1831
+ result.error = "Invalid output directory";
1832
+ return result;
1833
+ }
1834
+
1637
1835
  try {
1638
1836
  const fn = SVGToolbox[fnName];
1639
1837
  if (!fn) {
@@ -1846,10 +2044,18 @@ function parseArgs(args) {
1846
2044
  switch (arg) {
1847
2045
  case "-o":
1848
2046
  case "--output":
2047
+ if (i + 1 >= args.length) {
2048
+ logError("-o/--output requires a value");
2049
+ process.exit(CONSTANTS.EXIT_ERROR);
2050
+ }
1849
2051
  cfg.output = args[++i];
1850
2052
  break;
1851
2053
  case "-l":
1852
2054
  case "--list":
2055
+ if (i + 1 >= args.length) {
2056
+ logError("-l/--list requires a value");
2057
+ process.exit(CONSTANTS.EXIT_ERROR);
2058
+ }
1853
2059
  cfg.listFile = args[++i];
1854
2060
  break;
1855
2061
  case "-r":
@@ -1858,6 +2064,10 @@ function parseArgs(args) {
1858
2064
  break;
1859
2065
  case "-p":
1860
2066
  case "--precision": {
2067
+ if (i + 1 >= args.length) {
2068
+ logError("-p/--precision requires a value");
2069
+ process.exit(CONSTANTS.EXIT_ERROR);
2070
+ }
1861
2071
  const precision = parseInt(args[++i], 10);
1862
2072
  if (
1863
2073
  isNaN(precision) ||
@@ -1889,6 +2099,10 @@ function parseArgs(args) {
1889
2099
  cfg.verbose = true;
1890
2100
  break;
1891
2101
  case "--log-file":
2102
+ if (i + 1 >= args.length) {
2103
+ logError("--log-file requires a value");
2104
+ process.exit(CONSTANTS.EXIT_ERROR);
2105
+ }
1892
2106
  cfg.logFile = args[++i];
1893
2107
  break;
1894
2108
  case "-h":
@@ -1936,6 +2150,12 @@ function parseArgs(args) {
1936
2150
  break;
1937
2151
  // Namespace preservation option (comma-separated list)
1938
2152
  case "--preserve-ns": {
2153
+ if (!argValue && i + 1 >= args.length) {
2154
+ logError(
2155
+ "--preserve-ns requires a comma-separated list of namespaces",
2156
+ );
2157
+ process.exit(CONSTANTS.EXIT_ERROR);
2158
+ }
1939
2159
  const namespaces = argValue || args[++i];
1940
2160
  if (!namespaces) {
1941
2161
  logError(
@@ -1947,6 +2167,13 @@ function parseArgs(args) {
1947
2167
  .split(",")
1948
2168
  .map((ns) => ns.trim().toLowerCase())
1949
2169
  .filter((ns) => ns.length > 0);
2170
+ // Why: Reject empty array after filtering whitespace-only entries
2171
+ if (cfg.preserveNamespaces.length === 0) {
2172
+ logError(
2173
+ "--preserve-ns list is empty after filtering whitespace",
2174
+ );
2175
+ process.exit(CONSTANTS.EXIT_ERROR);
2176
+ }
1950
2177
  break;
1951
2178
  }
1952
2179
  case "--svg2-polyfills":
@@ -1958,6 +2185,10 @@ function parseArgs(args) {
1958
2185
  break;
1959
2186
  // E2E verification precision options
1960
2187
  case "--clip-segments": {
2188
+ if (i + 1 >= args.length) {
2189
+ logError("--clip-segments requires a value");
2190
+ process.exit(CONSTANTS.EXIT_ERROR);
2191
+ }
1961
2192
  const segs = parseInt(args[++i], 10);
1962
2193
  if (isNaN(segs) || segs < 8 || segs > 512) {
1963
2194
  logError("clip-segments must be between 8 and 512");
@@ -1967,15 +2198,28 @@ function parseArgs(args) {
1967
2198
  break;
1968
2199
  }
1969
2200
  case "--bezier-arcs": {
2201
+ if (i + 1 >= args.length) {
2202
+ logError("--bezier-arcs requires a value");
2203
+ process.exit(CONSTANTS.EXIT_ERROR);
2204
+ }
1970
2205
  const arcs = parseInt(args[++i], 10);
1971
2206
  if (isNaN(arcs) || arcs < 4 || arcs > 128) {
1972
2207
  logError("bezier-arcs must be between 4 and 128");
1973
2208
  process.exit(CONSTANTS.EXIT_ERROR);
1974
2209
  }
2210
+ // Why: bezier-arcs must be multiple of 4 for proper arc approximation
2211
+ if (arcs % 4 !== 0) {
2212
+ logError("bezier-arcs must be a multiple of 4");
2213
+ process.exit(CONSTANTS.EXIT_ERROR);
2214
+ }
1975
2215
  cfg.bezierArcs = arcs;
1976
2216
  break;
1977
2217
  }
1978
2218
  case "--e2e-tolerance": {
2219
+ if (i + 1 >= args.length) {
2220
+ logError("--e2e-tolerance requires a value");
2221
+ process.exit(CONSTANTS.EXIT_ERROR);
2222
+ }
1979
2223
  const tol = args[++i];
1980
2224
  if (!/^1e-\d+$/.test(tol)) {
1981
2225
  logError("e2e-tolerance must be in format 1e-N (e.g., 1e-10, 1e-12)");
@@ -2044,6 +2288,11 @@ function gatherInputFiles() {
2044
2288
  * @returns {string} Output file path
2045
2289
  */
2046
2290
  function getOutputPath(inputPath) {
2291
+ // Why: Validate parameter to prevent crashes
2292
+ if (!inputPath || typeof inputPath !== "string") {
2293
+ throw new Error("Invalid input path: must be a non-empty string");
2294
+ }
2295
+
2047
2296
  if (!config.output) {
2048
2297
  const dir = dirname(inputPath);
2049
2298
  const base = basename(inputPath, ".svg");