#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references using System; using System.Collections.Generic; using System.Globalization; using System.Text; namespace GitHub.Actions.WorkflowParser.Conversion { /// /// Builder for job and step IDs /// internal sealed class IdBuilder { internal void AppendSegment(String value) { if (String.IsNullOrEmpty(value)) { return; } if (m_name.Length == 0) { var first = value[0]; if ((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z') || first == '_') { // Legal first char } else if ((first >= '0' && first <= '9') || first == '-') { // Illegal first char, but legal char. // Prepend "_". m_name.Append("_"); } else { // Illegal char } } else { // Separator m_name.Append(c_separator); } foreach (var c in value) { if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-') { // Legal m_name.Append(c); } else { // Illegal m_name.Append("_"); } } } /// /// Builds the ID from the segments /// /// When true, generated IDs may begin with "__" depending upon the segments /// and collisions with known IDs. When false, generated IDs will never begin with the reserved prefix "__". /// The maximum length of the generated ID. internal String Build( Boolean allowReservedPrefix, Int32 maxLength = WorkflowConstants.MaxNodeNameLength) { // Ensure reasonable max length if (maxLength <= 5) // Must be long enough to accommodate at least one character + length of max suffix "_999" (refer suffix logic further below) { maxLength = WorkflowConstants.MaxNodeNameLength; } var original = m_name.Length > 0 ? m_name.ToString() : "job"; // Avoid prefix "__" when not allowed if (!allowReservedPrefix && original.StartsWith("__", StringComparison.Ordinal)) { original = $"_{original.TrimStart('_')}"; } var attempt = 1; var suffix = default(String); while (true) { if (attempt == 1) { suffix = String.Empty; } else if (attempt < 1000) { // Special case to avoid prefix "__" when not allowed if (!allowReservedPrefix && String.Equals(original, "_", StringComparison.Ordinal)) { suffix = String.Format(CultureInfo.InvariantCulture, "{0}", attempt); } else { suffix = String.Format(CultureInfo.InvariantCulture, "_{0}", attempt); } } else { throw new InvalidOperationException("Unable to create a unique name"); } var candidate = original.Substring(0, Math.Min(original.Length, maxLength - suffix.Length)) + suffix; if (m_distinctNames.Add(candidate)) { m_name.Clear(); return candidate; } attempt++; } } internal Boolean TryAddKnownId( String value, out String error) { if (String.IsNullOrEmpty(value) || !IsValid(value) || value.Length >= WorkflowConstants.MaxNodeNameLength) { error = $"The identifier '{value}' is invalid. IDs may only contain alphanumeric characters, '_', and '-'. IDs must start with a letter or '_' and must be less than {WorkflowConstants.MaxNodeNameLength} characters."; return false; } else if (value.StartsWith("__", StringComparison.Ordinal)) { error = $"The identifier '{value}' is invalid. IDs starting with '__' are reserved."; return false; } else if (!m_distinctNames.Add(value)) { error = $"The identifier '{value}' may not be used more than once within the same scope."; return false; } else { error = null; return true; } } private static Boolean IsValid(String name) { var result = true; for (Int32 i = 0; i < name.Length; i++) { if ((name[i] >= 'a' && name[i] <= 'z') || (name[i] >= 'A' && name[i] <= 'Z') || (name[i] >= '0' && name[i] <= '9' && i > 0) || (name[i] == '_') || (name[i] == '-' && i > 0)) { continue; } else { result = false; break; } } return result; } private const String c_separator = "_"; private readonly HashSet m_distinctNames = new HashSet(StringComparer.OrdinalIgnoreCase); private readonly StringBuilder m_name = new StringBuilder(); } }