using System; using System.Collections.Generic; using System.Text.RegularExpressions; using NcProgramManager.Cnc.Models; namespace NcProgramManager.Cnc { /// Validates NC program content per-manufacturer before upload. public sealed class NcProgramValidator : INcProgramValidator { private const int MaxBlockLength = 80; private const int MaxCommentLength = 32; private static readonly Regex FanucNamePattern = new Regex(@"^O\d{4}$", RegexOptions.Compiled); private static readonly Regex FanucFirstLinePattern = new Regex(@"^O\d{4}", RegexOptions.Compiled); private readonly CncManufacturer _manufacturer; public NcProgramValidator(CncManufacturer manufacturer) { _manufacturer = manufacturer; } public ValidationResult Validate(string programName, string content) { var errors = new List(); // Name-format check (uses just the bare filename without path) string name = ExtractName(programName); ValidateName(name, errors); if (string.IsNullOrEmpty(content)) return errors.Count == 0 ? ValidationResult.Ok() : ValidationResult.Fail(errors); // Normalise: strip CR, split on LF string normalized = content.Replace("\r", ""); string[] lines = normalized.Split('\n'); // First-line check (Fanuc/Mitsubishi) if ((_manufacturer == CncManufacturer.Fanuc || _manufacturer == CncManufacturer.Mitsubishi) && lines.Length > 0) { string first = lines[0].Trim(); if (!FanucFirstLinePattern.IsMatch(first)) errors.Add(new ValidationError(1, "First line must be O#### program number", ValidationSeverity.Error)); } bool restrictive = IsRestrictiveDialect(); for (int i = 0; i < lines.Length; i++) { int lineNum = i + 1; string line = lines[i]; if (restrictive) { // Tab check if (line.IndexOf('\t') >= 0) errors.Add(new ValidationError(lineNum, "Tab character not allowed", ValidationSeverity.Error)); // Lowercase check for (int ci = 0; ci < line.Length; ci++) { if (char.IsLower(line[ci])) { errors.Add(new ValidationError(lineNum, "Lowercase letters not allowed", ValidationSeverity.Error)); break; } } // Block length if (line.Length > MaxBlockLength) errors.Add(new ValidationError(lineNum, string.Format("Block length {0} exceeds 80 chars", line.Length), ValidationSeverity.Error)); // Nested parens + comment length ValidateParensAndComments(line, lineNum, errors); } // Forbidden chars per manufacturer ValidateForbiddenChars(line, lineNum, errors); } bool hasErrors = false; for (int i = 0; i < errors.Count; i++) if (errors[i].Severity == ValidationSeverity.Error) { hasErrors = true; break; } if (hasErrors) return ValidationResult.Fail(errors); // Preserve warnings even when overall result is Success return errors.Count == 0 ? ValidationResult.Ok() : ValidationResult.OkWithWarnings(errors); } // ── private helpers ─────────────────────────────────────────────────── private bool IsRestrictiveDialect() { return _manufacturer == CncManufacturer.Fanuc || _manufacturer == CncManufacturer.Mitsubishi; } private void ValidateName(string name, List errors) { if (string.IsNullOrEmpty(name)) return; switch (_manufacturer) { case CncManufacturer.Fanuc: case CncManufacturer.Mitsubishi: if (!FanucNamePattern.IsMatch(name)) errors.Add(new ValidationError(0, string.Format("Program name '{0}' must match O####", name), ValidationSeverity.Error)); break; case CncManufacturer.Siemens: if (!name.EndsWith(".MPF")) errors.Add(new ValidationError(0, string.Format("Program name '{0}' must end with .MPF", name), ValidationSeverity.Error)); break; case CncManufacturer.Heidenhain: if (!name.EndsWith(".H")) errors.Add(new ValidationError(0, string.Format("Program name '{0}' must end with .H", name), ValidationSeverity.Error)); break; case CncManufacturer.Fagor: // Estrai stem ed estensione (solo ultima estensione conta) int dotIdx = name.LastIndexOf('.'); string stem = dotIdx >= 0 ? name.Substring(0, dotIdx) : name; // Lunghezza: regola applicata sul nome completo (incluso eventuale .ext) if (name.Length > 24) errors.Add(new ValidationError(0, string.Format("Nome programma '{0}' supera 24 caratteri", name), ValidationSeverity.Error)); if (name.IndexOf('\u00f1') >= 0) errors.Add(new ValidationError(0, "Carattere '\u00f1' non ammesso nel nome programma Fagor", ValidationSeverity.Error)); // Stem deve contenere solo lettere/cifre/spazi for (int i = 0; i < stem.Length; i++) { char c = stem[i]; if (!(char.IsLetterOrDigit(c) || c == ' ')) { errors.Add(new ValidationError(0, string.Format("Carattere '{0}' non ammesso nel nome programma Fagor (solo lettere/cifre/spazi)", c), ValidationSeverity.Error)); break; } } break; } } private void ValidateParensAndComments(string line, int lineNum, List errors) { int depth = 0; int commentStart = -1; for (int i = 0; i < line.Length; i++) { char c = line[i]; if (c == '(') { depth++; if (depth > 1) { errors.Add(new ValidationError(lineNum, "Nested parentheses not allowed", ValidationSeverity.Error)); // Don't keep adding the same error for this line return; } commentStart = i; } else if (c == ')') { if (depth == 1 && commentStart >= 0) { // Extract comment content between the parens int len = i - commentStart - 1; if (len > MaxCommentLength) errors.Add(new ValidationError(lineNum, string.Format("Comment length {0} exceeds 32 chars", len), ValidationSeverity.Error)); } if (depth > 0) depth--; commentStart = -1; } } } private void ValidateForbiddenChars(string line, int lineNum, List errors) { switch (_manufacturer) { case CncManufacturer.Fanuc: case CncManufacturer.Mitsubishi: for (int i = 0; i < line.Length; i++) { char c = line[i]; if (c == ';' || c == '[' || c == ']') { errors.Add(new ValidationError(lineNum, string.Format("Forbidden char '{0}' for Fanuc/Mitsubishi", c), ValidationSeverity.Error)); } else if (c == '#') { errors.Add(new ValidationError(lineNum, "Macro variable '#' detected — verify macro support on target CNC", ValidationSeverity.Warning)); } } break; case CncManufacturer.Siemens: for (int i = 0; i < line.Length; i++) { if (line[i] == ';') errors.Add(new ValidationError(lineNum, "Forbidden char ';' for Siemens (use () for comments)", ValidationSeverity.Error)); } break; case CncManufacturer.Heidenhain: for (int i = 0; i < line.Length; i++) { char c = line[i]; if (c == '%' || c == ';') errors.Add(new ValidationError(lineNum, string.Format("Forbidden char '{0}' for Heidenhain", c), ValidationSeverity.Error)); } break; case CncManufacturer.Fagor: // Fagor: nessuna restrizione su contenuto programma (ISO + linguaggio alto livello) break; } } /// Extracts bare filename from a path (strips directory prefix). private static string ExtractName(string programName) { if (string.IsNullOrEmpty(programName)) return programName; int slash = programName.LastIndexOf('/'); int backslash = programName.LastIndexOf('\\'); int sep = slash > backslash ? slash : backslash; return sep >= 0 ? programName.Substring(sep + 1) : programName; } } }