Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Evolve.Cli/Evolve.Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<PublishTrimmed>true</PublishTrimmed>
<RootNamespace>EvolveDb.Cli</RootNamespace>
</PropertyGroup>
Expand Down
1 change: 1 addition & 0 deletions src/Evolve.Cli/EvolveFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public static Evolve Build(Program options, Action<string> logInfoDelegate = nul
RetryRepeatableMigrationsUntilNoError = options.RetryRepeatableMigrationsUntilNoError,
TransactionMode = options.TransactionMode,
SkipNextMigrations = options.SkipNextMigrations,
ChecksumAlgorithm = options.ChecksumAlgorithm,
};

if (options.Placeholders != null)
Expand Down
8 changes: 8 additions & 0 deletions src/Evolve.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Security.Cryptography;
using AllowedValuesAttribute = McMaster.Extensions.CommandLineUtils.AllowedValuesAttribute;

[Command(ResponseFileHandling = ResponseFileHandling.ParseArgsAsSpaceSeparated)]
Expand Down Expand Up @@ -129,6 +130,13 @@ private int OnExecute(CommandLineApplication app, IConsole console)
[Option("--skip-next-migrations", "When set, mark all subsequent migrations as applied. Default: false", CommandOptionType.SingleValue)]
public bool SkipNextMigrations { get; }

[Option("--checksum-algorithm", "Hash algorithm name (e.g., MD5, SHA256, SHA384, SHA512, SHA3-256). Must be supported by the system. Default: MD5", CommandOptionType.SingleValue)]
public string ChecksumAlgorithmName { get; } = "MD5";

public HashAlgorithmName ChecksumAlgorithm => string.IsNullOrWhiteSpace(ChecksumAlgorithmName)
? HashAlgorithmName.MD5
: new HashAlgorithmName(ChecksumAlgorithmName);

// Cassandra
[Option("--keyspace", "A list of keyspaces managed by Evolve (Cassandra only).", CommandOptionType.MultipleValue)]
public string[] Keyspaces { get; }
Expand Down
2 changes: 1 addition & 1 deletion src/Evolve.Tool/Evolve.Tool.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net9.0</TargetFrameworks>
<TargetFrameworks>net10.0</TargetFrameworks>
<PackAsTool>true</PackAsTool>
<ToolCommandName>evolve</ToolCommandName>
<RootNamespace>EvolveDb.Tool</RootNamespace>
Expand Down
17 changes: 16 additions & 1 deletion src/Evolve/Configuration/IEvolveConfiguration.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using EvolveDb.Migration;

Expand Down Expand Up @@ -214,9 +215,23 @@ public interface IEvolveConfiguration
bool SkipNextMigrations { get; }

/// <summary>
/// A custom <see cref="IMigrationLoader"/> used to load all migrations (applied, pending, ignored...)
/// A custom <see cref="IMigrationLoader"/> used to load all migrations (applied, pending, ignored...)
/// that replaces the built-in ones (<see cref="FileMigrationLoader"/> <see cref="EmbeddedResourceMigrationLoader"/>)
/// </summary>
IMigrationLoader MigrationLoader { get; }

/// <summary>
/// <para>
/// Hash algorithm used for calculating migration checksums. (default: MD5)
/// </para>
/// <para>
/// SHA-256, SHA-384, and SHA-512 are FIPS-compliant alternatives.
/// </para>
/// <para>
/// Note: When validating checksums, Evolve will accept both the configured
/// algorithm and MD5 for backward compatibility with existing databases.
/// </para>
/// </summary>
HashAlgorithmName ChecksumAlgorithm { get; }
}
}
2 changes: 1 addition & 1 deletion src/Evolve/Dialect/CockroachDb/CockroachDbMetadataTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ protected override void InternalCreate()
"version VARCHAR(50), " +
"description VARCHAR(200) NOT NULL, " +
"name VARCHAR(300) NOT NULL, " +
"checksum VARCHAR(32), " +
"checksum VARCHAR(128), " +
"installed_by VARCHAR(100) NOT NULL, " +
"installed_on TIMESTAMP NOT NULL DEFAULT now(), " +
"success BOOLEAN NOT NULL " +
Expand Down
2 changes: 1 addition & 1 deletion src/Evolve/Dialect/MySQL/MySQLMetadataTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ protected override void InternalCreate()
"version VARCHAR(50), " +
"description VARCHAR(200) NOT NULL, " +
"name VARCHAR(300) NOT NULL, " +
"checksum VARCHAR(32), " +
"checksum VARCHAR(128), " +
"installed_by VARCHAR(100) NOT NULL, " +
"installed_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
"success BOOL NOT NULL " +
Expand Down
2 changes: 1 addition & 1 deletion src/Evolve/Dialect/PostgreSQL/PostgreSQLMetadataTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ protected override void InternalCreate()
"version VARCHAR(50), " +
"description VARCHAR(200) NOT NULL, " +
"name VARCHAR(300) NOT NULL, " +
"checksum VARCHAR(32), " +
"checksum VARCHAR(128), " +
"installed_by VARCHAR(100) NOT NULL, " +
"installed_on TIMESTAMP NOT NULL DEFAULT now(), " +
"success BOOLEAN NOT NULL " +
Expand Down
2 changes: 1 addition & 1 deletion src/Evolve/Dialect/SQLServer/SQLServerMetadataTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ protected override void InternalCreate()
"version VARCHAR(50), " +
"description VARCHAR(200) NOT NULL, " +
"name VARCHAR(300) NOT NULL, " +
"checksum VARCHAR(32), " +
"checksum VARCHAR(128), " +
"installed_by VARCHAR(100) NOT NULL, " +
"installed_on DATETIME NOT NULL DEFAULT GETDATE(), " +
"success BIT NOT NULL " +
Expand Down
2 changes: 1 addition & 1 deletion src/Evolve/Dialect/SQLite/SQLiteMetadataTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ protected override void InternalCreate()
"version VARCHAR(50), " +
"description VARCHAR(200) NOT NULL, " +
"name VARCHAR(300) NOT NULL, " +
"checksum VARCHAR(32), " +
"checksum VARCHAR(128), " +
"installed_by VARCHAR(100) NOT NULL, " +
"installed_on TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f','now')), " +
"success BOOLEAN NOT NULL " +
Expand Down
3 changes: 3 additions & 0 deletions src/Evolve/Evolve.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Transactions;
Expand Down Expand Up @@ -90,6 +91,8 @@ public IMigrationLoader MigrationLoader
set { _migrationLoader = value; }
}

public HashAlgorithmName ChecksumAlgorithm { get; set; } = HashAlgorithmName.MD5;

#endregion

#region Properties
Expand Down
2 changes: 1 addition & 1 deletion src/Evolve/Evolve.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Import Project="..\..\build\common.props" />

<PropertyGroup>
<TargetFrameworks>net9.0</TargetFrameworks>
<TargetFrameworks>net10.0</TargetFrameworks>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<LangVersion>latest</LangVersion>
Expand Down
12 changes: 10 additions & 2 deletions src/Evolve/Migration/EmbeddedResourceMigrationLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,11 @@ public virtual IEnumerable<MigrationScript> GetMigrations()
encoding);
})
.ToList()
.ForEach(x => migrations.Add(x));
.ForEach(x =>
{
x.HashAlgorithm = _options.ChecksumAlgorithm;
migrations.Add(x);
});
}

return migrations.CheckForDuplicateVersion()
Expand Down Expand Up @@ -100,7 +104,11 @@ public virtual IEnumerable<MigrationScript> GetRepeatableMigrations()
encoding);
})
.ToList()
.ForEach(x => migrations.Add(x));
.ForEach(x =>
{
x.HashAlgorithm = _options.ChecksumAlgorithm;
migrations.Add(x);
});
}

return migrations.CheckForDuplicateName()
Expand Down
12 changes: 10 additions & 2 deletions src/Evolve/Migration/FileMigrationLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,11 @@ public virtual IEnumerable<MigrationScript> GetMigrations()
return new FileMigrationScript(path: f.FullName, version, description, MetadataType.Migration, encoding);
})
.ToList()
.ForEach(x => migrations.Add(x));
.ForEach(x =>
{
x.HashAlgorithm = _options.ChecksumAlgorithm;
migrations.Add(x);
});
}

return migrations.CheckForDuplicateVersion()
Expand Down Expand Up @@ -100,7 +104,11 @@ public virtual IEnumerable<MigrationScript> GetRepeatableMigrations()
return new FileMigrationScript(f.FullName, version: null, description, MetadataType.RepeatableMigration, encoding);
})
.ToList()
.ForEach(x => migrations.Add(x));
.ForEach(x =>
{
x.HashAlgorithm = _options.ChecksumAlgorithm;
migrations.Add(x);
});
}

return migrations.CheckForDuplicateName()
Expand Down
22 changes: 16 additions & 6 deletions src/Evolve/Migration/FileMigrationScript.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,22 +44,32 @@ public override void ValidateChecksum(string? checksum)
}
catch
{
if (checksum != FallbackCheck())
// Try fallback methods for backward compatibility (pre v1.8.0 hashing of file stream)
if (checksum == FallbackCheck() ||
checksum == FallbackCheckWithAlgorithm(HashAlgorithm))
{
throw;
return;
}
throw;
}
}

/// <summary>
/// Calculate the checksum with the pre v1.8.0 version.
/// Calculate the checksum with the pre v1.8.0 version (reads file directly with MD5).
/// </summary>
private string FallbackCheck()
{
using var md5 = MD5.Create();
return FallbackCheckWithAlgorithm(HashAlgorithmName.MD5);
}

/// <summary>
/// Calculate the checksum with the pre v1.8.0 version using specified algorithm.
/// </summary>
/// <param name="algorithm"> The hash algorithm to use. </param>
private string FallbackCheckWithAlgorithm(HashAlgorithmName algorithm)
{
using FileStream stream = File.OpenRead(Path);
byte[] checksum = md5.ComputeHash(stream);
return BitConverter.ToString(checksum).Replace("-", string.Empty);
return ChecksumCalculator.Calculate(stream, algorithm);
}
}
}
54 changes: 47 additions & 7 deletions src/Evolve/Migration/MigrationScript.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ protected MigrationScript(string? version, string description, string name, stri
/// </summary>
public string Content { get; }

/// <summary>
/// Gets or sets the hash algorithm used for checksum calculation.
/// </summary>
internal HashAlgorithmName HashAlgorithm { get; set; } = HashAlgorithmName.MD5;

/// <summary>
/// Returns false if the special comment "evolve-tx-off" is found in the first line of the script, true otherwise.
/// </summary>
Expand All @@ -37,29 +42,64 @@ protected MigrationScript(string? version, string description, string name, stri

/// <summary>
/// Validates the <paramref name="checksum"/> against the actual migration one.
/// Throws on mismatch.
/// Supports dual validation: accepts both the configured algorithm and MD5 for backward compatibility.
/// </summary>
/// <param name="checksum"> The applied migration checksum. </param>
/// <exception cref="EvolveValidationException"></exception>
public virtual void ValidateChecksum(string? checksum)
{
Check.NotNull(checksum, nameof(checksum));

if (checksum != CalculateChecksum())
string currentChecksum = CalculateChecksum();

// Direct match with configured algorithm
if (checksum == currentChecksum)
{
return;
}

// Dual validation: try MD5 for backward compatibility if using SHA-2
if (HashAlgorithm != HashAlgorithmName.MD5)
{
string md5Checksum = CalculateChecksumWithAlgorithm(HashAlgorithmName.MD5);
if (checksum == md5Checksum)
{
return;
}
}

// Detect stored algorithm and recalculate if different
var detectedAlgorithm = ChecksumCalculator.DetectAlgorithm(checksum);
if (detectedAlgorithm.HasValue && detectedAlgorithm.Value != HashAlgorithm)
{
throw new EvolveValidationException(string.Format(IncorrectMigrationChecksum, Name));
string recalculated = CalculateChecksumWithAlgorithm(detectedAlgorithm.Value);
if (checksum == recalculated)
{
return;
}
}

throw new EvolveValidationException(string.Format(IncorrectMigrationChecksum, Name));
}

/// <summary>
/// Returns the checksum where <code>crlf</code> and <code>lf</code> line endings have been previously normalized to <code>lf</code>.
/// </summary>
/// <returns></returns>
/// <returns> The checksum calculated with the configured hash algorithm. </returns>
public virtual string CalculateChecksum()
{
using var md5 = MD5.Create();
byte[] checksum = md5.ComputeHash(Encoding.UTF8.GetBytes(NormalizeLineEndings(Content)));
return BitConverter.ToString(checksum).Replace("-", string.Empty);
return CalculateChecksumWithAlgorithm(HashAlgorithm);
}

/// <summary>
/// Calculates checksum using a specific algorithm.
/// </summary>
/// <param name="algorithm"> The hash algorithm to use. </param>
/// <returns> The checksum calculated with the specified algorithm. </returns>
protected string CalculateChecksumWithAlgorithm(HashAlgorithmName algorithm)
{
string normalizedContent = NormalizeLineEndings(Content);
return ChecksumCalculator.Calculate(normalizedContent, algorithm);
}

/// <summary>
Expand Down
Loading