diff --git a/CarManagerV3/Classes/Car.cs b/CarManagerV3/Classes/Car.cs index 431da59..599bd1e 100644 --- a/CarManagerV3/Classes/Car.cs +++ b/CarManagerV3/Classes/Car.cs @@ -1,4 +1,5 @@ using System; +using CarManagerV3.Util; namespace CarManagerV3 { @@ -115,12 +116,15 @@ namespace CarManagerV3 int numericId = 0; if ((string.IsNullOrWhiteSpace(id) || int.TryParse(id, out numericId)) && id != "0") { - id = Guid.NewGuid().ToString(); + id = CUID.NewCUID().ToString(); if (numericId > 0) { order = numericId + order; } - + } + if(id.Length > 8) + { + id = CUID.NewCUID().ToString(); } // Sets the properties using the setters to ensure validation is applied. this.id = id; diff --git a/CarManagerV3/Forms/MainForm.Designer.cs b/CarManagerV3/Forms/MainForm.Designer.cs index fcba4da..52d8aad 100644 --- a/CarManagerV3/Forms/MainForm.Designer.cs +++ b/CarManagerV3/Forms/MainForm.Designer.cs @@ -29,171 +29,170 @@ private void InitializeComponent() { System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm)); - this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); - this.flpCars = new System.Windows.Forms.FlowLayoutPanel(); - this.tableLayoutPanel2 = new System.Windows.Forms.TableLayoutPanel(); - this.tbxSearch = new System.Windows.Forms.TextBox(); - this.btnNewCar = new System.Windows.Forms.Button(); - this.menuStrip1 = new System.Windows.Forms.MenuStrip(); - this.fileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.openToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.saveToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.saveAsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.importToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.recentFilesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.revealInFileExplorerToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.tableLayoutPanel1.SuspendLayout(); - this.tableLayoutPanel2.SuspendLayout(); - this.menuStrip1.SuspendLayout(); - this.SuspendLayout(); + tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); + flpCars = new System.Windows.Forms.FlowLayoutPanel(); + tableLayoutPanel2 = new System.Windows.Forms.TableLayoutPanel(); + tbxSearch = new System.Windows.Forms.TextBox(); + btnNewCar = new System.Windows.Forms.Button(); + menuStrip1 = new System.Windows.Forms.MenuStrip(); + fileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + openToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + saveToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + saveAsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + importToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + recentFilesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + revealInFileExplorerToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + tableLayoutPanel1.SuspendLayout(); + tableLayoutPanel2.SuspendLayout(); + menuStrip1.SuspendLayout(); + SuspendLayout(); // // tableLayoutPanel1 // - this.tableLayoutPanel1.ColumnCount = 1; - this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); - this.tableLayoutPanel1.Controls.Add(this.flpCars, 0, 2); - this.tableLayoutPanel1.Controls.Add(this.tableLayoutPanel2, 0, 1); - this.tableLayoutPanel1.Controls.Add(this.menuStrip1, 0, 0); - this.tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill; - this.tableLayoutPanel1.Location = new System.Drawing.Point(0, 0); - this.tableLayoutPanel1.Name = "tableLayoutPanel1"; - this.tableLayoutPanel1.RowCount = 3; - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 40F)); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tableLayoutPanel1.Size = new System.Drawing.Size(802, 458); - this.tableLayoutPanel1.TabIndex = 0; - this.tableLayoutPanel1.Paint += new System.Windows.Forms.PaintEventHandler(this.tableLayoutPanel1_Paint); + tableLayoutPanel1.ColumnCount = 1; + tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); + tableLayoutPanel1.Controls.Add(flpCars, 0, 2); + tableLayoutPanel1.Controls.Add(tableLayoutPanel2, 0, 1); + tableLayoutPanel1.Controls.Add(menuStrip1, 0, 0); + tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill; + tableLayoutPanel1.Location = new System.Drawing.Point(0, 0); + tableLayoutPanel1.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); + tableLayoutPanel1.Name = "tableLayoutPanel1"; + tableLayoutPanel1.RowCount = 3; + tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); + tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 50F)); + tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); + tableLayoutPanel1.Size = new System.Drawing.Size(802, 572); + tableLayoutPanel1.TabIndex = 0; + tableLayoutPanel1.Paint += tableLayoutPanel1_Paint; // // flpCars // - this.flpCars.AutoScroll = true; - this.flpCars.AutoScrollMargin = new System.Drawing.Size(0, 200); - this.flpCars.Dock = System.Windows.Forms.DockStyle.Fill; - this.flpCars.Location = new System.Drawing.Point(3, 67); - this.flpCars.Name = "flpCars"; - this.flpCars.Size = new System.Drawing.Size(796, 412); - this.flpCars.TabIndex = 1; + flpCars.AutoScroll = true; + flpCars.AutoScrollMargin = new System.Drawing.Size(0, 200); + flpCars.Dock = System.Windows.Forms.DockStyle.Fill; + flpCars.Location = new System.Drawing.Point(3, 82); + flpCars.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); + flpCars.Name = "flpCars"; + flpCars.Size = new System.Drawing.Size(796, 515); + flpCars.TabIndex = 1; // // tableLayoutPanel2 // - this.tableLayoutPanel2.ColumnCount = 2; - this.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)); - this.tableLayoutPanel2.Controls.Add(this.tbxSearch, 0, 0); - this.tableLayoutPanel2.Controls.Add(this.btnNewCar, 1, 0); - this.tableLayoutPanel2.Dock = System.Windows.Forms.DockStyle.Fill; - this.tableLayoutPanel2.Location = new System.Drawing.Point(3, 27); - this.tableLayoutPanel2.Name = "tableLayoutPanel2"; - this.tableLayoutPanel2.RowCount = 1; - this.tableLayoutPanel2.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); - this.tableLayoutPanel2.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 34F)); - this.tableLayoutPanel2.Size = new System.Drawing.Size(796, 34); - this.tableLayoutPanel2.TabIndex = 2; + tableLayoutPanel2.ColumnCount = 2; + 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)); + tableLayoutPanel2.Controls.Add(tbxSearch, 0, 0); + tableLayoutPanel2.Controls.Add(btnNewCar, 1, 0); + tableLayoutPanel2.Dock = System.Windows.Forms.DockStyle.Fill; + tableLayoutPanel2.Location = new System.Drawing.Point(3, 32); + tableLayoutPanel2.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); + tableLayoutPanel2.Name = "tableLayoutPanel2"; + tableLayoutPanel2.RowCount = 1; + tableLayoutPanel2.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); + tableLayoutPanel2.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 42F)); + tableLayoutPanel2.Size = new System.Drawing.Size(796, 42); + tableLayoutPanel2.TabIndex = 2; // // tbxSearch // - this.tbxSearch.Dock = System.Windows.Forms.DockStyle.Fill; - this.tbxSearch.Location = new System.Drawing.Point(3, 3); - this.tbxSearch.Name = "tbxSearch"; - this.tbxSearch.Size = new System.Drawing.Size(392, 22); - this.tbxSearch.TabIndex = 3; - this.tbxSearch.TextChanged += new System.EventHandler(this.tbxSearch_TextChanged); + tbxSearch.Dock = System.Windows.Forms.DockStyle.Fill; + tbxSearch.Location = new System.Drawing.Point(3, 4); + tbxSearch.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); + tbxSearch.Name = "tbxSearch"; + tbxSearch.Size = new System.Drawing.Size(392, 27); + tbxSearch.TabIndex = 3; + tbxSearch.TextChanged += tbxSearch_TextChanged; // // btnNewCar // - this.btnNewCar.Location = new System.Drawing.Point(401, 3); - this.btnNewCar.Name = "btnNewCar"; - this.btnNewCar.Size = new System.Drawing.Size(75, 23); - this.btnNewCar.TabIndex = 4; - this.btnNewCar.Text = "Add Car"; - this.btnNewCar.UseVisualStyleBackColor = true; - this.btnNewCar.Click += new System.EventHandler(this.btnNewCar_Click); + btnNewCar.Location = new System.Drawing.Point(401, 4); + btnNewCar.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); + btnNewCar.Name = "btnNewCar"; + btnNewCar.Size = new System.Drawing.Size(75, 29); + btnNewCar.TabIndex = 4; + btnNewCar.Text = "Add Car"; + btnNewCar.UseVisualStyleBackColor = true; + btnNewCar.Click += btnNewCar_Click; // // menuStrip1 // - this.menuStrip1.ImageScalingSize = new System.Drawing.Size(20, 20); - this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.fileToolStripMenuItem}); - this.menuStrip1.Location = new System.Drawing.Point(0, 0); - this.menuStrip1.Name = "menuStrip1"; - this.menuStrip1.Size = new System.Drawing.Size(802, 24); - this.menuStrip1.TabIndex = 3; - this.menuStrip1.Text = "menuStrip1"; + menuStrip1.ImageScalingSize = new System.Drawing.Size(20, 20); + menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { fileToolStripMenuItem }); + menuStrip1.Location = new System.Drawing.Point(0, 0); + menuStrip1.Name = "menuStrip1"; + menuStrip1.Size = new System.Drawing.Size(802, 28); + menuStrip1.TabIndex = 3; + menuStrip1.Text = "menuStrip1"; // // fileToolStripMenuItem // - this.fileToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.openToolStripMenuItem, - this.saveToolStripMenuItem, - this.saveAsToolStripMenuItem, - this.importToolStripMenuItem, - this.recentFilesToolStripMenuItem, - this.revealInFileExplorerToolStripMenuItem}); - this.fileToolStripMenuItem.Name = "fileToolStripMenuItem"; - this.fileToolStripMenuItem.Size = new System.Drawing.Size(37, 20); - this.fileToolStripMenuItem.Text = "File"; + fileToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { openToolStripMenuItem, saveToolStripMenuItem, saveAsToolStripMenuItem, importToolStripMenuItem, recentFilesToolStripMenuItem, revealInFileExplorerToolStripMenuItem }); + fileToolStripMenuItem.Name = "fileToolStripMenuItem"; + fileToolStripMenuItem.Size = new System.Drawing.Size(46, 24); + fileToolStripMenuItem.Text = "File"; // // openToolStripMenuItem // - this.openToolStripMenuItem.Name = "openToolStripMenuItem"; - this.openToolStripMenuItem.Size = new System.Drawing.Size(187, 22); - this.openToolStripMenuItem.Text = "Open"; - this.openToolStripMenuItem.Click += new System.EventHandler(this.openToolStripMenuItem_Click); + openToolStripMenuItem.Name = "openToolStripMenuItem"; + openToolStripMenuItem.Size = new System.Drawing.Size(238, 26); + openToolStripMenuItem.Text = "Open"; + openToolStripMenuItem.Click += openToolStripMenuItem_Click; // // saveToolStripMenuItem // - this.saveToolStripMenuItem.Name = "saveToolStripMenuItem"; - this.saveToolStripMenuItem.Size = new System.Drawing.Size(187, 22); - this.saveToolStripMenuItem.Text = "Save"; - this.saveToolStripMenuItem.Click += new System.EventHandler(this.saveToolStripMenuItem_Click); + saveToolStripMenuItem.Name = "saveToolStripMenuItem"; + saveToolStripMenuItem.Size = new System.Drawing.Size(238, 26); + saveToolStripMenuItem.Text = "Save"; + saveToolStripMenuItem.Click += saveToolStripMenuItem_Click; // // saveAsToolStripMenuItem // - this.saveAsToolStripMenuItem.Name = "saveAsToolStripMenuItem"; - this.saveAsToolStripMenuItem.Size = new System.Drawing.Size(187, 22); - this.saveAsToolStripMenuItem.Text = "Save as"; - this.saveAsToolStripMenuItem.Click += new System.EventHandler(this.saveAsToolStripMenuItem_Click); + saveAsToolStripMenuItem.Name = "saveAsToolStripMenuItem"; + saveAsToolStripMenuItem.Size = new System.Drawing.Size(238, 26); + saveAsToolStripMenuItem.Text = "Save as"; + saveAsToolStripMenuItem.Click += saveAsToolStripMenuItem_Click; // // importToolStripMenuItem // - this.importToolStripMenuItem.Name = "importToolStripMenuItem"; - this.importToolStripMenuItem.Size = new System.Drawing.Size(187, 22); - this.importToolStripMenuItem.Text = "Import"; - this.importToolStripMenuItem.Click += new System.EventHandler(this.importToolStripMenuItem_Click); + importToolStripMenuItem.Name = "importToolStripMenuItem"; + importToolStripMenuItem.Size = new System.Drawing.Size(238, 26); + importToolStripMenuItem.Text = "Import"; + importToolStripMenuItem.Click += importToolStripMenuItem_Click; // // recentFilesToolStripMenuItem // - this.recentFilesToolStripMenuItem.Name = "recentFilesToolStripMenuItem"; - this.recentFilesToolStripMenuItem.Size = new System.Drawing.Size(187, 22); - this.recentFilesToolStripMenuItem.Text = "Recent Files"; - this.recentFilesToolStripMenuItem.Click += new System.EventHandler(this.recentFilesToolStripMenuItem_Click); + recentFilesToolStripMenuItem.Name = "recentFilesToolStripMenuItem"; + recentFilesToolStripMenuItem.Size = new System.Drawing.Size(238, 26); + recentFilesToolStripMenuItem.Text = "Recent Files"; + recentFilesToolStripMenuItem.Click += recentFilesToolStripMenuItem_Click; // // revealInFileExplorerToolStripMenuItem // - this.revealInFileExplorerToolStripMenuItem.Name = "revealInFileExplorerToolStripMenuItem"; - this.revealInFileExplorerToolStripMenuItem.Size = new System.Drawing.Size(187, 22); - this.revealInFileExplorerToolStripMenuItem.Text = "Reveal in File Explorer"; - this.revealInFileExplorerToolStripMenuItem.Click += new System.EventHandler(this.revealInFileExplorerToolStripMenuItem_Click); + revealInFileExplorerToolStripMenuItem.Name = "revealInFileExplorerToolStripMenuItem"; + revealInFileExplorerToolStripMenuItem.Size = new System.Drawing.Size(238, 26); + revealInFileExplorerToolStripMenuItem.Text = "Reveal in File Explorer"; + revealInFileExplorerToolStripMenuItem.Click += revealInFileExplorerToolStripMenuItem_Click; // // MainForm // - this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(802, 458); - this.Controls.Add(this.tableLayoutPanel1); - this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); - this.MainMenuStrip = this.menuStrip1; - this.MinimumSize = new System.Drawing.Size(818, 497); - this.Name = "MainForm"; - this.Text = "Carmanager 3"; - this.tableLayoutPanel1.ResumeLayout(false); - this.tableLayoutPanel1.PerformLayout(); - this.tableLayoutPanel2.ResumeLayout(false); - this.tableLayoutPanel2.PerformLayout(); - this.menuStrip1.ResumeLayout(false); - this.menuStrip1.PerformLayout(); - this.ResumeLayout(false); + AutoScaleDimensions = new System.Drawing.SizeF(8F, 20F); + AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + ClientSize = new System.Drawing.Size(802, 572); + Controls.Add(tableLayoutPanel1); + Icon = (System.Drawing.Icon)resources.GetObject("$this.Icon"); + MainMenuStrip = menuStrip1; + Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); + MinimumSize = new System.Drawing.Size(818, 609); + Name = "MainForm"; + Text = "Carmanager 3"; + tableLayoutPanel1.ResumeLayout(false); + tableLayoutPanel1.PerformLayout(); + tableLayoutPanel2.ResumeLayout(false); + tableLayoutPanel2.PerformLayout(); + menuStrip1.ResumeLayout(false); + menuStrip1.PerformLayout(); + ResumeLayout(false); } diff --git a/CarManagerV3/Forms/MainForm.cs b/CarManagerV3/Forms/MainForm.cs index 257ff5b..5366660 100644 --- a/CarManagerV3/Forms/MainForm.cs +++ b/CarManagerV3/Forms/MainForm.cs @@ -139,6 +139,7 @@ namespace CarManagerV3 int temp = car.Order; car.Order = other.Order; other.Order = temp; + cars = StateManager.normalizeOrders(cars); SafeManager.SaveCars(filepath, cars); refreshCars(cars); } @@ -155,6 +156,7 @@ namespace CarManagerV3 int temp = car.Order; car.Order = other.Order; other.Order = temp; + cars = StateManager.normalizeOrders(cars); SafeManager.SaveCars(filepath, cars); refreshCars(cars); } diff --git a/CarManagerV3/Forms/MainForm.resx b/CarManagerV3/Forms/MainForm.resx index bb1bb0c..aaace22 100644 --- a/CarManagerV3/Forms/MainForm.resx +++ b/CarManagerV3/Forms/MainForm.resx @@ -1,17 +1,17 @@  - diff --git a/CarManagerV3/Util/CUID.cs b/CarManagerV3/Util/CUID.cs new file mode 100644 index 0000000..12e6901 --- /dev/null +++ b/CarManagerV3/Util/CUID.cs @@ -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); + + /// + /// 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. + /// + /// The desired length of the generated CUID + /// Whether to prefix the CUID with 'c' for better readability and to avoid starting with a digit." + /// + 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 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 dst32) + { + // Compose a payload with: + // - 16 bytes random + // - 8 bytes timestamp (UTC ticks) + // - 4 bytes counter + // - 4 bytes process/thread noise + Span 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 seed32, int bytesNeeded) + { + if (bytesNeeded <= 0) return Array.Empty(); + + // 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 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 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('='); + } + + } +}