feat: CUID & fix: reorder bug

This commit is contained in:
2026-03-02 16:34:18 +01:00
parent 9b261dbf78
commit f0d51bc85e
5 changed files with 275 additions and 152 deletions

View File

@@ -1,4 +1,5 @@
using System; using System;
using CarManagerV3.Util;
namespace CarManagerV3 namespace CarManagerV3
{ {
@@ -115,12 +116,15 @@ namespace CarManagerV3
int numericId = 0; int numericId = 0;
if ((string.IsNullOrWhiteSpace(id) || int.TryParse(id, out numericId)) && id != "0") if ((string.IsNullOrWhiteSpace(id) || int.TryParse(id, out numericId)) && id != "0")
{ {
id = Guid.NewGuid().ToString(); id = CUID.NewCUID().ToString();
if (numericId > 0) if (numericId > 0)
{ {
order = numericId + order; order = numericId + order;
} }
}
if(id.Length > 8)
{
id = CUID.NewCUID().ToString();
} }
// Sets the properties using the setters to ensure validation is applied. // Sets the properties using the setters to ensure validation is applied.
this.id = id; this.id = id;

View File

@@ -29,171 +29,170 @@
private void InitializeComponent() private void InitializeComponent()
{ {
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm)); System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm));
this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel();
this.flpCars = new System.Windows.Forms.FlowLayoutPanel(); flpCars = new System.Windows.Forms.FlowLayoutPanel();
this.tableLayoutPanel2 = new System.Windows.Forms.TableLayoutPanel(); tableLayoutPanel2 = new System.Windows.Forms.TableLayoutPanel();
this.tbxSearch = new System.Windows.Forms.TextBox(); tbxSearch = new System.Windows.Forms.TextBox();
this.btnNewCar = new System.Windows.Forms.Button(); btnNewCar = new System.Windows.Forms.Button();
this.menuStrip1 = new System.Windows.Forms.MenuStrip(); menuStrip1 = new System.Windows.Forms.MenuStrip();
this.fileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); fileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.openToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); openToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.saveToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); saveToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.saveAsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); saveAsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.importToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); importToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.recentFilesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); recentFilesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.revealInFileExplorerToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); revealInFileExplorerToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.tableLayoutPanel1.SuspendLayout(); tableLayoutPanel1.SuspendLayout();
this.tableLayoutPanel2.SuspendLayout(); tableLayoutPanel2.SuspendLayout();
this.menuStrip1.SuspendLayout(); menuStrip1.SuspendLayout();
this.SuspendLayout(); SuspendLayout();
// //
// tableLayoutPanel1 // tableLayoutPanel1
// //
this.tableLayoutPanel1.ColumnCount = 1; tableLayoutPanel1.ColumnCount = 1;
this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
this.tableLayoutPanel1.Controls.Add(this.flpCars, 0, 2); tableLayoutPanel1.Controls.Add(flpCars, 0, 2);
this.tableLayoutPanel1.Controls.Add(this.tableLayoutPanel2, 0, 1); tableLayoutPanel1.Controls.Add(tableLayoutPanel2, 0, 1);
this.tableLayoutPanel1.Controls.Add(this.menuStrip1, 0, 0); tableLayoutPanel1.Controls.Add(menuStrip1, 0, 0);
this.tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill; tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill;
this.tableLayoutPanel1.Location = new System.Drawing.Point(0, 0); tableLayoutPanel1.Location = new System.Drawing.Point(0, 0);
this.tableLayoutPanel1.Name = "tableLayoutPanel1"; tableLayoutPanel1.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
this.tableLayoutPanel1.RowCount = 3; tableLayoutPanel1.Name = "tableLayoutPanel1";
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); tableLayoutPanel1.RowCount = 3;
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 40F)); tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle());
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 50F));
this.tableLayoutPanel1.Size = new System.Drawing.Size(802, 458); tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle());
this.tableLayoutPanel1.TabIndex = 0; tableLayoutPanel1.Size = new System.Drawing.Size(802, 572);
this.tableLayoutPanel1.Paint += new System.Windows.Forms.PaintEventHandler(this.tableLayoutPanel1_Paint); tableLayoutPanel1.TabIndex = 0;
tableLayoutPanel1.Paint += tableLayoutPanel1_Paint;
// //
// flpCars // flpCars
// //
this.flpCars.AutoScroll = true; flpCars.AutoScroll = true;
this.flpCars.AutoScrollMargin = new System.Drawing.Size(0, 200); flpCars.AutoScrollMargin = new System.Drawing.Size(0, 200);
this.flpCars.Dock = System.Windows.Forms.DockStyle.Fill; flpCars.Dock = System.Windows.Forms.DockStyle.Fill;
this.flpCars.Location = new System.Drawing.Point(3, 67); flpCars.Location = new System.Drawing.Point(3, 82);
this.flpCars.Name = "flpCars"; flpCars.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
this.flpCars.Size = new System.Drawing.Size(796, 412); flpCars.Name = "flpCars";
this.flpCars.TabIndex = 1; flpCars.Size = new System.Drawing.Size(796, 515);
flpCars.TabIndex = 1;
// //
// tableLayoutPanel2 // tableLayoutPanel2
// //
this.tableLayoutPanel2.ColumnCount = 2; tableLayoutPanel2.ColumnCount = 2;
this.tableLayoutPanel2.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F)); tableLayoutPanel2.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F));
this.tableLayoutPanel2.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F)); tableLayoutPanel2.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F));
this.tableLayoutPanel2.Controls.Add(this.tbxSearch, 0, 0); tableLayoutPanel2.Controls.Add(tbxSearch, 0, 0);
this.tableLayoutPanel2.Controls.Add(this.btnNewCar, 1, 0); tableLayoutPanel2.Controls.Add(btnNewCar, 1, 0);
this.tableLayoutPanel2.Dock = System.Windows.Forms.DockStyle.Fill; tableLayoutPanel2.Dock = System.Windows.Forms.DockStyle.Fill;
this.tableLayoutPanel2.Location = new System.Drawing.Point(3, 27); tableLayoutPanel2.Location = new System.Drawing.Point(3, 32);
this.tableLayoutPanel2.Name = "tableLayoutPanel2"; tableLayoutPanel2.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
this.tableLayoutPanel2.RowCount = 1; tableLayoutPanel2.Name = "tableLayoutPanel2";
this.tableLayoutPanel2.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); tableLayoutPanel2.RowCount = 1;
this.tableLayoutPanel2.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 34F)); tableLayoutPanel2.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
this.tableLayoutPanel2.Size = new System.Drawing.Size(796, 34); tableLayoutPanel2.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 42F));
this.tableLayoutPanel2.TabIndex = 2; tableLayoutPanel2.Size = new System.Drawing.Size(796, 42);
tableLayoutPanel2.TabIndex = 2;
// //
// tbxSearch // tbxSearch
// //
this.tbxSearch.Dock = System.Windows.Forms.DockStyle.Fill; tbxSearch.Dock = System.Windows.Forms.DockStyle.Fill;
this.tbxSearch.Location = new System.Drawing.Point(3, 3); tbxSearch.Location = new System.Drawing.Point(3, 4);
this.tbxSearch.Name = "tbxSearch"; tbxSearch.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
this.tbxSearch.Size = new System.Drawing.Size(392, 22); tbxSearch.Name = "tbxSearch";
this.tbxSearch.TabIndex = 3; tbxSearch.Size = new System.Drawing.Size(392, 27);
this.tbxSearch.TextChanged += new System.EventHandler(this.tbxSearch_TextChanged); tbxSearch.TabIndex = 3;
tbxSearch.TextChanged += tbxSearch_TextChanged;
// //
// btnNewCar // btnNewCar
// //
this.btnNewCar.Location = new System.Drawing.Point(401, 3); btnNewCar.Location = new System.Drawing.Point(401, 4);
this.btnNewCar.Name = "btnNewCar"; btnNewCar.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
this.btnNewCar.Size = new System.Drawing.Size(75, 23); btnNewCar.Name = "btnNewCar";
this.btnNewCar.TabIndex = 4; btnNewCar.Size = new System.Drawing.Size(75, 29);
this.btnNewCar.Text = "Add Car"; btnNewCar.TabIndex = 4;
this.btnNewCar.UseVisualStyleBackColor = true; btnNewCar.Text = "Add Car";
this.btnNewCar.Click += new System.EventHandler(this.btnNewCar_Click); btnNewCar.UseVisualStyleBackColor = true;
btnNewCar.Click += btnNewCar_Click;
// //
// menuStrip1 // menuStrip1
// //
this.menuStrip1.ImageScalingSize = new System.Drawing.Size(20, 20); menuStrip1.ImageScalingSize = new System.Drawing.Size(20, 20);
this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { fileToolStripMenuItem });
this.fileToolStripMenuItem}); menuStrip1.Location = new System.Drawing.Point(0, 0);
this.menuStrip1.Location = new System.Drawing.Point(0, 0); menuStrip1.Name = "menuStrip1";
this.menuStrip1.Name = "menuStrip1"; menuStrip1.Size = new System.Drawing.Size(802, 28);
this.menuStrip1.Size = new System.Drawing.Size(802, 24); menuStrip1.TabIndex = 3;
this.menuStrip1.TabIndex = 3; menuStrip1.Text = "menuStrip1";
this.menuStrip1.Text = "menuStrip1";
// //
// fileToolStripMenuItem // fileToolStripMenuItem
// //
this.fileToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { fileToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { openToolStripMenuItem, saveToolStripMenuItem, saveAsToolStripMenuItem, importToolStripMenuItem, recentFilesToolStripMenuItem, revealInFileExplorerToolStripMenuItem });
this.openToolStripMenuItem, fileToolStripMenuItem.Name = "fileToolStripMenuItem";
this.saveToolStripMenuItem, fileToolStripMenuItem.Size = new System.Drawing.Size(46, 24);
this.saveAsToolStripMenuItem, fileToolStripMenuItem.Text = "File";
this.importToolStripMenuItem,
this.recentFilesToolStripMenuItem,
this.revealInFileExplorerToolStripMenuItem});
this.fileToolStripMenuItem.Name = "fileToolStripMenuItem";
this.fileToolStripMenuItem.Size = new System.Drawing.Size(37, 20);
this.fileToolStripMenuItem.Text = "File";
// //
// openToolStripMenuItem // openToolStripMenuItem
// //
this.openToolStripMenuItem.Name = "openToolStripMenuItem"; openToolStripMenuItem.Name = "openToolStripMenuItem";
this.openToolStripMenuItem.Size = new System.Drawing.Size(187, 22); openToolStripMenuItem.Size = new System.Drawing.Size(238, 26);
this.openToolStripMenuItem.Text = "Open"; openToolStripMenuItem.Text = "Open";
this.openToolStripMenuItem.Click += new System.EventHandler(this.openToolStripMenuItem_Click); openToolStripMenuItem.Click += openToolStripMenuItem_Click;
// //
// saveToolStripMenuItem // saveToolStripMenuItem
// //
this.saveToolStripMenuItem.Name = "saveToolStripMenuItem"; saveToolStripMenuItem.Name = "saveToolStripMenuItem";
this.saveToolStripMenuItem.Size = new System.Drawing.Size(187, 22); saveToolStripMenuItem.Size = new System.Drawing.Size(238, 26);
this.saveToolStripMenuItem.Text = "Save"; saveToolStripMenuItem.Text = "Save";
this.saveToolStripMenuItem.Click += new System.EventHandler(this.saveToolStripMenuItem_Click); saveToolStripMenuItem.Click += saveToolStripMenuItem_Click;
// //
// saveAsToolStripMenuItem // saveAsToolStripMenuItem
// //
this.saveAsToolStripMenuItem.Name = "saveAsToolStripMenuItem"; saveAsToolStripMenuItem.Name = "saveAsToolStripMenuItem";
this.saveAsToolStripMenuItem.Size = new System.Drawing.Size(187, 22); saveAsToolStripMenuItem.Size = new System.Drawing.Size(238, 26);
this.saveAsToolStripMenuItem.Text = "Save as"; saveAsToolStripMenuItem.Text = "Save as";
this.saveAsToolStripMenuItem.Click += new System.EventHandler(this.saveAsToolStripMenuItem_Click); saveAsToolStripMenuItem.Click += saveAsToolStripMenuItem_Click;
// //
// importToolStripMenuItem // importToolStripMenuItem
// //
this.importToolStripMenuItem.Name = "importToolStripMenuItem"; importToolStripMenuItem.Name = "importToolStripMenuItem";
this.importToolStripMenuItem.Size = new System.Drawing.Size(187, 22); importToolStripMenuItem.Size = new System.Drawing.Size(238, 26);
this.importToolStripMenuItem.Text = "Import"; importToolStripMenuItem.Text = "Import";
this.importToolStripMenuItem.Click += new System.EventHandler(this.importToolStripMenuItem_Click); importToolStripMenuItem.Click += importToolStripMenuItem_Click;
// //
// recentFilesToolStripMenuItem // recentFilesToolStripMenuItem
// //
this.recentFilesToolStripMenuItem.Name = "recentFilesToolStripMenuItem"; recentFilesToolStripMenuItem.Name = "recentFilesToolStripMenuItem";
this.recentFilesToolStripMenuItem.Size = new System.Drawing.Size(187, 22); recentFilesToolStripMenuItem.Size = new System.Drawing.Size(238, 26);
this.recentFilesToolStripMenuItem.Text = "Recent Files"; recentFilesToolStripMenuItem.Text = "Recent Files";
this.recentFilesToolStripMenuItem.Click += new System.EventHandler(this.recentFilesToolStripMenuItem_Click); recentFilesToolStripMenuItem.Click += recentFilesToolStripMenuItem_Click;
// //
// revealInFileExplorerToolStripMenuItem // revealInFileExplorerToolStripMenuItem
// //
this.revealInFileExplorerToolStripMenuItem.Name = "revealInFileExplorerToolStripMenuItem"; revealInFileExplorerToolStripMenuItem.Name = "revealInFileExplorerToolStripMenuItem";
this.revealInFileExplorerToolStripMenuItem.Size = new System.Drawing.Size(187, 22); revealInFileExplorerToolStripMenuItem.Size = new System.Drawing.Size(238, 26);
this.revealInFileExplorerToolStripMenuItem.Text = "Reveal in File Explorer"; revealInFileExplorerToolStripMenuItem.Text = "Reveal in File Explorer";
this.revealInFileExplorerToolStripMenuItem.Click += new System.EventHandler(this.revealInFileExplorerToolStripMenuItem_Click); revealInFileExplorerToolStripMenuItem.Click += revealInFileExplorerToolStripMenuItem_Click;
// //
// MainForm // MainForm
// //
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F); AutoScaleDimensions = new System.Drawing.SizeF(8F, 20F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(802, 458); ClientSize = new System.Drawing.Size(802, 572);
this.Controls.Add(this.tableLayoutPanel1); Controls.Add(tableLayoutPanel1);
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); Icon = (System.Drawing.Icon)resources.GetObject("$this.Icon");
this.MainMenuStrip = this.menuStrip1; MainMenuStrip = menuStrip1;
this.MinimumSize = new System.Drawing.Size(818, 497); Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
this.Name = "MainForm"; MinimumSize = new System.Drawing.Size(818, 609);
this.Text = "Carmanager 3"; Name = "MainForm";
this.tableLayoutPanel1.ResumeLayout(false); Text = "Carmanager 3";
this.tableLayoutPanel1.PerformLayout(); tableLayoutPanel1.ResumeLayout(false);
this.tableLayoutPanel2.ResumeLayout(false); tableLayoutPanel1.PerformLayout();
this.tableLayoutPanel2.PerformLayout(); tableLayoutPanel2.ResumeLayout(false);
this.menuStrip1.ResumeLayout(false); tableLayoutPanel2.PerformLayout();
this.menuStrip1.PerformLayout(); menuStrip1.ResumeLayout(false);
this.ResumeLayout(false); menuStrip1.PerformLayout();
ResumeLayout(false);
} }

View File

@@ -139,6 +139,7 @@ namespace CarManagerV3
int temp = car.Order; int temp = car.Order;
car.Order = other.Order; car.Order = other.Order;
other.Order = temp; other.Order = temp;
cars = StateManager.normalizeOrders(cars);
SafeManager.SaveCars(filepath, cars); SafeManager.SaveCars(filepath, cars);
refreshCars(cars); refreshCars(cars);
} }
@@ -155,6 +156,7 @@ namespace CarManagerV3
int temp = car.Order; int temp = car.Order;
car.Order = other.Order; car.Order = other.Order;
other.Order = temp; other.Order = temp;
cars = StateManager.normalizeOrders(cars);
SafeManager.SaveCars(filepath, cars); SafeManager.SaveCars(filepath, cars);
refreshCars(cars); refreshCars(cars);
} }

118
CarManagerV3/Util/CUID.cs Normal file
View File

@@ -0,0 +1,118 @@
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace CarManagerV3.Util
{
internal class CUID
{
public const int DefaultLength = 6;
private static int _counter = RandomNumberGenerator.GetInt32(int.MaxValue);
/// <summary>
/// Generate a random CUID (Collision-resistant Unique Identifier) of a specified length.
/// The CUID is designed to be unique across different machines and time, making it suitable for use as an identifier for cars in the application.
/// The length must be between 4 and 32 characters to ensure a good balance between uniqueness and readability.
/// The generated CUID consists of a combination of alphanumeric characters to ensure uniqueness and readability.
/// </summary>
/// <param name="length">The desired length of the generated CUID</param>
/// <param name="prefixWithC">Whether to prefix the CUID with 'c' for better readability and to avoid starting with a digit.</param>"
/// <returns></returns>
public static string NewCUID(int length = DefaultLength, bool prefixWithC = true)
{
// CUIDv2 specs allow between 4 and 32 chars.
if(length < 4 || length > 32) throw new ArgumentOutOfRangeException("length");
// We will produce enough encoded chars to satisfy 'length' after prefixing and truncation.
// Base64 encodes 3 bytes -> 4 chars. So bytesNeeded ≈ ceil(charsNeeded * 3/4).
int charsNeeded = prefixWithC ? (length - 1) : length;
int bytesNeeded = (int)Math.Ceiling(charsNeeded * 3.0 / 4.0);
Span<byte> material = stackalloc byte[32];
FillMaterial(material);
byte[] outputBytes = ExpandWithSha256(material, bytesNeeded);
string encoded = Base64UrlEncode(outputBytes);
if (encoded.Length < charsNeeded)
{
// Extremely unlikely unless length is huge; ensure we have enough by expanding more.
// (Kept as a guard; for typical lengths like 24-64, you're fine.)
outputBytes = ExpandWithSha256(material, bytesNeeded + 32);
encoded = Base64UrlEncode(outputBytes);
}
string body = encoded.Substring(0, charsNeeded);
return prefixWithC ? ("c" + body) : body;
}
private static void FillMaterial(Span<byte> dst32)
{
// Compose a payload with:
// - 16 bytes random
// - 8 bytes timestamp (UTC ticks)
// - 4 bytes counter
// - 4 bytes process/thread noise
Span<byte> payload = stackalloc byte[16 + 8 + 4 + 4];
RandomNumberGenerator.Fill(payload.Slice(0, 16));
long ticks = DateTime.UtcNow.Ticks;
BinaryPrimitives.WriteInt64LittleEndian(payload.Slice(16, 8), ticks);
int c = Interlocked.Increment(ref _counter);
BinaryPrimitives.WriteInt32LittleEndian(payload.Slice(24, 4), c);
// Some extra variability (not relied on for security)
int noise = Environment.ProcessId ^ Thread.CurrentThread.ManagedThreadId ^ (int)Stopwatch.GetTimestamp();
BinaryPrimitives.WriteInt32LittleEndian(payload.Slice(28, 4), noise);
// Hash to produce 32 bytes of uniformly distributed output
SHA256.HashData(payload, dst32);
}
private static byte[] ExpandWithSha256(ReadOnlySpan<byte> seed32, int bytesNeeded)
{
if (bytesNeeded <= 0) return Array.Empty<byte>();
// If <= 32 bytes, we can just take from seed32 by hashing once more for separation.
// We'll use SHA256(seed || blockIndex) to generate blocks.
int blocks = (int)Math.Ceiling(bytesNeeded / 32.0);
byte[] result = new byte[blocks * 32];
Span<byte> input = stackalloc byte[32 + 4];
seed32.CopyTo(input.Slice(0, 32));
for (int i = 0; i < blocks; i++)
{
BinaryPrimitives.WriteInt32LittleEndian(input.Slice(32, 4), i);
Span<byte> block = result.AsSpan(i * 32, 32);
SHA256.HashData(input, block);
}
if (result.Length == bytesNeeded) return result;
byte[] trimmed = new byte[bytesNeeded];
Buffer.BlockCopy(result, 0, trimmed, 0, bytesNeeded);
return trimmed;
}
private static string Base64UrlEncode(byte[] bytes)
{
// Standard base64url without padding per RFC 4648 §5
string b64 = Convert.ToBase64String(bytes);
return b64.Replace('+', '-').Replace('/', '_').TrimEnd('=');
}
}
}