nc_program_manager/Cnc/Mazak/FtpListParser.cs
dtrentin 89487256d7 fix(mazak): harden FTP list parsing + surface root-list failure
- FtpListParser: tolerant DOS/IIS regex (optional AM/PM, 2-or-4-digit
  year, 1-or-2-digit hour) for 24h/locale-dependent Mazak servers;
  NLST fallback now only accepts single-token lines (multi-token
  garbage/header -> skip) to avoid whole-line-as-filename.
- MazakMachine.BuildTreeAsync: root-level LIST failure now propagates
  -> Fail (no more silent exit 0 on a diagnostic probe). Children keep
  best-effort inline "errore lista" swallow. Depth semantics preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 15:14:57 +02:00

91 lines
3.6 KiB
C#

using System;
using System.Globalization;
using System.Text.RegularExpressions;
namespace NcProgramManager.Cnc.Mazak
{
/// <summary>
/// Parsing puro (senza I/O) delle righe LIST/ListDirectoryDetails FTP dei
/// controlli Mazak Smooth/Matrix. Supporta formato Unix (drwx...) e DOS/IIS
/// (MM-DD-YY ... &lt;DIR&gt;), piu' fallback NLST (solo nome).
/// </summary>
internal static class FtpListParser
{
/// <summary>Assicura path con slash iniziale e finale per uso come directory.</summary>
public static string NormalizeDir(string path)
{
string dir = string.IsNullOrEmpty(path) ? "/" : path;
if (!dir.StartsWith("/")) dir = "/" + dir;
if (!dir.EndsWith("/")) dir += "/";
return dir;
}
// DOS/IIS: "07-18-26 10:00AM <DIR> NAME"
// "07-18-26 10:00AM 1234 FILE.EIA"
private static readonly Regex _dosLine = new Regex(
@"^\d{2}-\d{2}-\d{2,4}\s+\d{1,2}:\d{2}\s*(AM|PM)?\s+(?<dir><DIR>|\d+)\s+(?<name>.+)$",
RegexOptions.IgnoreCase);
// Whitespace chars used to detect multi-token (non-NLST) lines in fallback.
private static readonly char[] _whitespace = { ' ', '\t', '\v', '\f' };
// Unix: "drwxr-xr-x 1 owner group 4096 Jul 18 10:00 NAME"
private static readonly Regex _unixLine = new Regex(
@"^(?<perm>[dl\-][rwxsStT\-]{9})\s+\d+\s+\S+\s+\S+\s+(?<size>\d+)\s+\S+\s+\S+\s+\S+\s+(?<name>.+)$");
/// <summary>Parsa una riga LIST. Ritorna null se non riconosciuta o '.'/'..'.</summary>
public static FtpEntry Parse(string line, string dir)
{
if (string.IsNullOrWhiteSpace(line)) return null;
string name = null;
bool isDir = false;
long size = -1;
var dos = _dosLine.Match(line);
if (dos.Success)
{
name = dos.Groups["name"].Value.Trim();
if (string.Equals(dos.Groups["dir"].Value, "<DIR>", StringComparison.OrdinalIgnoreCase))
isDir = true;
else
long.TryParse(dos.Groups["dir"].Value, out size);
}
else
{
var unix = _unixLine.Match(line);
if (unix.Success)
{
name = unix.Groups["name"].Value.Trim();
isDir = unix.Groups["perm"].Value[0] == 'd';
if (!isDir)
long.TryParse(unix.Groups["size"].Value, NumberStyles.Integer,
CultureInfo.InvariantCulture, out size);
}
else
{
// Fallback: NLST-style (bare name only). Only a single token
// (no internal whitespace) is a real name-only listing; a line
// with internal whitespace that matched neither regex is an
// unrecognized header/garbage -> skip (avoid whole-line-as-name).
string trimmed = line.Trim();
if (trimmed.IndexOfAny(_whitespace) >= 0)
return null;
name = trimmed;
}
}
if (string.IsNullOrEmpty(name) || name == "." || name == "..")
return null;
return new FtpEntry
{
Name = name,
FullPath = dir + name,
IsDirectory = isDir,
Size = size,
Raw = line
};
}
}
}