114 lines
4.1 KiB
C#
114 lines
4.1 KiB
C#
namespace AnchorCli.Hashline;
|
|
|
|
/// <summary>
|
|
/// Validates and resolves Hashline anchors of the form "{lineNumber}:{hash}" against
|
|
/// the current state of a file's lines.
|
|
///
|
|
/// An anchor is valid if and only if BOTH conditions hold:
|
|
/// 1. The referenced line number exists in the file.
|
|
/// 2. The hash at that line number matches what the encoder would produce today.
|
|
///
|
|
/// This dual-check makes stale edits fail loudly rather than silently corrupting a
|
|
/// different line that happens to share the same hash.
|
|
/// </summary>
|
|
internal static class HashlineValidator
|
|
{
|
|
/// <summary>
|
|
/// Parses a "{lineNumber}:{hash}" anchor string.
|
|
/// </summary>
|
|
/// <returns>True and the parsed values if the format is valid; false otherwise.</returns>
|
|
public static bool TryParseAnchor(string anchor, out int lineNumber, out string hash)
|
|
{
|
|
lineNumber = 0;
|
|
hash = string.Empty;
|
|
|
|
int colonIndex = anchor.IndexOf(':');
|
|
if (colonIndex <= 0 || colonIndex == anchor.Length - 1)
|
|
return false;
|
|
|
|
ReadOnlySpan<char> numPart = anchor.AsSpan(0, colonIndex);
|
|
ReadOnlySpan<char> hashPart = anchor.AsSpan(colonIndex + 1);
|
|
|
|
if (!int.TryParse(numPart, out lineNumber) || lineNumber < 1)
|
|
return false;
|
|
|
|
hash = hashPart.ToString();
|
|
return hash.Length == 2;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves an anchor to a 0-based index into <paramref name="lines"/>,
|
|
/// verifying both the line number and the content hash.
|
|
/// </summary>
|
|
/// <param name="anchor">Anchor string, e.g. "5:a3".</param>
|
|
/// <param name="lines">Current file lines (without newlines).</param>
|
|
/// <param name="zeroBasedIndex">Resolved 0-based index on success.</param>
|
|
/// <param name="error">Human-readable error message on failure.</param>
|
|
/// <returns>True if the anchor is valid and current; false otherwise.</returns>
|
|
public static bool TryResolve(
|
|
string anchor,
|
|
string[] lines,
|
|
out int zeroBasedIndex,
|
|
out string error)
|
|
{
|
|
zeroBasedIndex = -1;
|
|
error = string.Empty;
|
|
|
|
if (!TryParseAnchor(anchor, out int lineNumber, out string expectedHash))
|
|
{
|
|
error = $"Anchor '{anchor}' is not in 'lineNumber:hash' format (e.g. '5:a3').";
|
|
return false;
|
|
}
|
|
|
|
if (lineNumber < 1 || lineNumber > lines.Length)
|
|
{
|
|
error = $"Anchor '{anchor}': line {lineNumber} is out of range. Re-read the file ({lines.Length} lines).";
|
|
return false;
|
|
}
|
|
|
|
string actualHash = HashlineEncoder.ComputeHash(lines[lineNumber - 1].AsSpan(), lineNumber);
|
|
if (!string.Equals(actualHash, expectedHash, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
error = $"Anchor '{anchor}': hash mismatch at line {lineNumber}. The file has changed — re-read before editing.";
|
|
return false;
|
|
}
|
|
|
|
zeroBasedIndex = lineNumber - 1;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates both a start and end anchor, and ensures start <= end.
|
|
/// </summary>
|
|
/// <param name="startAnchor">The starting anchor string.</param>
|
|
/// <param name="endAnchor">The ending anchor string.</param>
|
|
/// <param name="lines">Current file lines (without newlines).</param>
|
|
/// <param name="startIndex">Resolved 0-based start index on success.</param>
|
|
/// <param name="endIndex">Resolved 0-based end index on success.</param>
|
|
/// <param name="error">Human-readable error message on failure.</param>
|
|
/// <returns>True if the range is valid; false otherwise.</returns>
|
|
public static bool TryResolveRange(
|
|
string startAnchor,
|
|
string endAnchor,
|
|
string[] lines,
|
|
out int startIndex,
|
|
out int endIndex,
|
|
out string error)
|
|
{
|
|
endIndex = -1;
|
|
if (!TryResolve(startAnchor, lines, out startIndex, out error))
|
|
return false;
|
|
|
|
if (!TryResolve(endAnchor, lines, out endIndex, out error))
|
|
return false;
|
|
|
|
if (startIndex > endIndex)
|
|
{
|
|
error = $"Range error: start anchor is after end anchor.";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|