//
//  dsemver -- Doj's Semantic Version parser and comparator.
//
//      Implements parsing and comparisons of "semantic versions" as specified
//      in the Semantic Versioning 2.0.0.
//
//      For details see https://semver.org/spec/v2.0.0.html
//
//      Semantic version format:
//
//          MAJOR.MINOR.PATCH[-prerelease][+build]
//
{$MODE FPC}
{$MODESWITCH DEFAULTPARAMETERS}
{$MODESWITCH OUT}
{$MODESWITCH RESULT}
unit dsemver;

interface

uses
  dparser;

type
// Record for storing parsing result.
// It does not own memory, just links to source string, so be sure the
// lifespan of the record is inside lifespan of the source.
TSemVerParsed = record
  Major, Minor, Patch: Cardinal;
  // if prerelease part is ommited than PreRelease=nil
  PreRelease: PAnsiChar;
  PreReleaseLen: SizeUInt;
  // if build metadata part is ommited than Build=nil
  Build: PAnsiChar;
  BuildLen: SizeUInt;
end;

//
//  SemVerParse
//
//      Parses a string and stores semantic version fields into SemVer
//      structure. Returns False if failed.
//
//  Parameters
//
//      S: beginning of parsing string
//      SEnd: end of the parsing string
//
//  Returns
//
//      Result: True for success, False otherwise
//      SemVer: parsed semver if Result=True
//
function SemVerParse(S, SEnd: PAnsiChar;
                     out SemVer: TSemVerParsed): Boolean;

//
//  SemVerValidate
//
//      Checks if the string contains correct Semantic Version.
//
//  Parameters:
//
//      S: the beginning of the string
//      SEnd: the end of the string
//
//  Returns:
//
//      Result: True if the string contains correct semantic version,
//              False otherwise
//
function SemVerValidate(S, SEnd: PAnsiChar): Boolean; inline;

//
//  SemVerCompare
//
//      Compares two semvers (as specified in the specification).
//
//  Returns:
//
//      Result: -1: SemVer1 < SemVer2
//               0: SemVer1 = SemVer2
//               1: SemVer1 > SemVer2
//
function SemVerCompare(constref SemVer1, SemVer2: TSemVerParsed): LongInt; overload;
function SemVerCompare(S1, S1End, S2, S2End: PAnsiChar): LongInt; overload;

//
//  SemVerPrec
//
//      Checks whenever SemVer1 <= SemVer2
//
function SemVerPrec(constref SemVer1, SemVer2: TSemVerParsed): Boolean; inline;
function SemVerPrec(S1, S1End, S2, S2End: PAnsiChar): Boolean; inline;

//
//  SemVerCheck
//
//      Checks for compatibility (as specified in the specification).
//
//  Parameters:
//
//      Version:  actual version of a software
//      Required: required version of the software
//
//  Returns:
//
//      Result: True if the software is backward compatible with the required
//              version
//
function SemVerCheck(constref Version, Required: TSemVerParsed): Boolean;
function SemVerCheck(Version, VersionEnd, Required, RequiredEnd: PAnsiChar): Boolean;

implementation

//
//  SemVerSkipSegment
//
//      Searches for end of a segment. It is either '.' character or SEnd.
//
//  Returns:
//
//      Result: pointer to first '.' character or SEnd
//      OnlyDigits: True if the segment contains only [0-9] characters
//
function SemVerSkipSegment(S, SEnd: PAnsiChar;
                           out OnlyDigits: Boolean): PAnsiChar;
begin
  OnlyDigits := True;
  while S < SEnd do begin
    case S^ of
      '.': Exit(S);
      '0'..'9': Inc(S);
    else
      OnlyDigits := False;
      Inc(S);
      Break;
    end;
  end;
  Result := S;
  SkipToChar(Result, SEnd, '.');
end;

function Min(A, B: SizeUInt): SizeUInt; inline;
begin
  if A < B then begin
    Result := A;
  end else
    Result := B;
end;

function CompareLexicographically(S1, S1End, S2, S2End: PAnsiChar): SizeInt;
var
  Len: SizeUInt;
begin
  if S1End - S1 > S2End - S2 then
    Exit(- CompareLexicographically(S2, S2End, S1, S1End));
  Len := S1End - S1;
  Result := CompareByte(S1^, S2^, Len);
  if Result = 0 then begin
    if Len = S2End - S2 then begin
      Exit(0);
    end else begin
      // S2 is longer than S1
      Exit(-1);
    end;
  end else if Result < 0 then begin
    Exit(-1);
  end else begin // if Result > 0
    Exit(1);
  end;
end;

function CompareInts(I1, I2: LongInt; out Res: LongInt): Boolean; inline;
begin
  if I1 < I2 then begin
    Res := -1;
    Exit(True);
  end else if I1 > I2 then begin
    Res := 1;
    Exit(True);
  end else begin
    Res := 0;
    Exit(False);
  end;
end;

function CompareNumbers(S1, S1End, S2, S2End: PAnsiChar): LongInt;
begin
  SkipChar(S1, S1End, '0');
  SkipChar(S2, S2End, '0');
  if CompareInts(S1End - S1, S2End - S2, Result) then
    Exit;
  Result := CompareLexicographically(S1, S1End, S2, S2End);
end;

function SemVerComparePreRelease(S1, S1End: PAnsiChar;
                                 S2, S2End: PAnsiChar): LongInt;
var
  OnlyDigits1, OnlyDigits2: Boolean;
  Seg1, Seg2: PAnsiChar;
begin
  while True do begin
    if S1 >= S1End then begin
      if S2 >= S2End then begin
        Exit(0);
      end else
        Exit(-1);
    end;
    if S2 >= S2End then
      Exit(1);
    Seg1 := S1;
    Seg2 := S2;
    S1 := SemVerSkipSegment(S1, S1End, OnlyDigits1);
    S2 := SemVerSkipSegment(S2, S2End, OnlyDigits2);
    if OnlyDigits1 then begin
      if OnlyDigits2 then begin
        Result := CompareNumbers(Seg1, S1, Seg2, S2);
        if Result <> 0 then
          Exit;
      end else
        Exit(-1);
    end else begin
      if not OnlyDigits2 then begin
        Result := CompareLexicographically(Seg1, S1, Seg2, S2);
        if Result <> 0 then
          Exit;
      end else
        Exit(1);
    end;
    if S1 < S1End then
      Inc(S1); // skip '.'
    if S2 < S2End then
      Inc(S2); // skip '.'
  end;
end;

function SemVerParseUInt(var S: PAnsiChar; SEnd: PAnsiChar; out U: Cardinal): Boolean;
begin
  Result := ParseDecimalU32(S, SEnd, U);
  if Result and (S < SEnd) and (S^ = '.') then
    Inc(S);
end;

function SemVerParseMajor(var S: PAnsiChar; SEnd: PAnsiChar): LongInt;
var
  U: Cardinal;
begin
  if not SemVerParseUInt(S, SEnd, U) then begin
    Exit(-1)
  end else
    Result := U;
end;

function SemVerParseMinor(var S: PAnsiChar; SEnd: PAnsiChar): LongInt;
begin
  Result := SemVerParseMajor(S, SEnd);
end;

function SemVerParsePatch(var S: PAnsiChar; SEnd: PAnsiChar): LongInt;
begin
  Result := SemVerParseMajor(S, SEnd);
end;

function SemVerParse(S, SEnd: PAnsiChar;
                     out SemVer: TSemVerParsed): Boolean;
begin
  SemVer.Major := SemVerParseMajor(S, SEnd);
  SemVer.Minor := SemVerParseMinor(S, SEnd);
  SemVer.Patch := SemVerParsePatch(S, SEnd);
  if (S < SEnd) and (S^ = '-') then begin
    Inc(S);
    SemVer.PreRelease := S;
    while S < SEnd do begin
      if not (S^ in ['0'..'9', 'a'..'z', 'A'..'Z', '-', '.']) then
        break;
      Inc(S);
    end;
    SemVer.PreReleaseLen := S - SemVer.PreRelease;
  end else begin
    SemVer.PreRelease := nil;
    SemVer.PreReleaseLen := 0;
  end;
  if (S < SEnd) and (S^ = '+') then begin
    Inc(S);
    SemVer.Build := S;
    while S < SEnd do begin
      if not (S^ in ['0'..'9', 'a'..'z', 'A'..'Z', '-', '.']) then
        break;
      Inc(S);
    end;
    SemVer.BuildLen := S - SemVer.Build;
  end else begin
    SemVer.Build := nil;
    SemVer.BuildLen := 0;
  end;
  Result := True;
end;

function SemVerValidate(S, SEnd: PAnsiChar): Boolean;
var
  SemVer: TSemVerParsed;
begin
  Result := SemVerParse(S, SEnd, SemVer);
end;

function SemVerCompare(constref SemVer1, SemVer2: TSemVerParsed): LongInt;
begin
  if SemVer1.Major <> SemVer2.Major then begin
    if SemVer1.Major < SemVer2.Major then begin
      Exit(-1);
    end else
      Exit(1);
  end;
  if SemVer1.Minor <> SemVer2.Minor then begin
    if SemVer1.Minor < SemVer2.Minor then begin
      Exit(-1);
    end else
      Exit(1);
  end;
  if SemVer1.Patch <> SemVer2.Patch then begin
    if SemVer1.Patch < SemVer2.Patch then begin
      Exit(-1);
    end else
      Exit(1);
  end;
  if SemVer1.PreRelease = nil then begin
    if SemVer2.PreRelease = nil then begin
      Exit(0);
    end else
      Exit(1);
  end else if SemVer2.PreRelease = nil then
    Exit(-1);
  Result := SemVerComparePreRelease(SemVer1.PreRelease,
                                    SemVer1.PreRelease + SemVer1.PreReleaseLen,
                                    SemVer2.PreRelease,
                                    SemVer2.PreRelease + SemVer2.PreReleaseLen);
end;

function SemVerCompare(S1, S1End, S2, S2End: PAnsiChar): LongInt;
var
  V1, V2: TSemVerParsed;
begin
  if SemVerParse(S1, S1End, V1) then begin
    if SemVerParse(S2, S2End, V2) then begin
      Result := SemVerCompare(V1, V2);
    end else
      Exit(1);
  end else if SemVerParse(S2, S2End, V2) then begin
    Exit(-1);
  end else
    Exit(0);
end;

function SemVerPrec(constref SemVer1, SemVer2: TSemVerParsed): Boolean;
begin
  Result := SemVerCompare(SemVer1, SemVer2) <= 0;
end;

function SemVerPrec(S1, S1End, S2, S2End: PAnsiChar): Boolean; inline;
begin
  Result := SemVerCompare(S1, S1End, S2, S2End) <= 0;
end;

function SemVerCheck(constref Version, Required: TSemVerParsed): Boolean;
begin
  Result := (Version.Major = Required.Major) and
            ((Version.Major = 0) or SemVerPrec(Required, Version));
end;

function SemVerCheck(Version, VersionEnd, Required, RequiredEnd: PAnsiChar): Boolean;
var
  V1, V2: TSemVerParsed;
begin
  if SemVerParse(Version, VersionEnd, V1) then begin
    if SemVerParse(Required, RequiredEnd, V2) then begin
      Result := SemVerCheck(V1, V2);
    end else
      Exit(False);
  end else begin
    // no version - no garantees
    Exit(False);
  end;
end;

end.
