feature/cuid #1

Merged
frozd merged 5 commits from feature/cuid into master 2026-03-03 11:33:29 +01:00
5 changed files with 275 additions and 152 deletions
Showing only changes of commit f0d51bc85e - Show all commits

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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);
}

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('=');
}
}
}