commit 3b180ee2f0f2f1ae435cf230e335628bb8b054ce Author: PANDA Project Date: Fri Aug 7 11:21:03 2026 +0200 Initial commit: PANDA 1.3 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c3a9859 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +* text=auto +*.cs text eol=crlf +*.ps1 text eol=crlf +*.md text eol=lf +*.csv text eol=crlf +*.png binary +*.ico binary +*.exe binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9781072 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Kompilierte Ausgaben +PANDA-Portable.exe +PANDA-Setup.exe +PANDA.Tests.exe +PANDA-Uninstall.Payload.exe +*.pdb + +# Automatisch erzeugte Oberflächenvorschauen +panda-preview.png +panda-import-assistent.png +panda-setup.png + +# Lokale Exporte und temporäre Tests +export.csv +*_veraendert.csv +portable-test/ + +# Editor- und Betriebssystemdateien +.vs/ +*.user +*.suo +Thumbs.db +Desktop.ini diff --git a/Beispiel.csv b/Beispiel.csv new file mode 100644 index 0000000..6e4ad2e --- /dev/null +++ b/Beispiel.csv @@ -0,0 +1,5 @@ +Kundennummer;Vorname;Nachname;Ort;Interne Notiz +1001;Anna;Meyer;Berlin;Priorität A +1002;Jonas;Schmidt;Hamburg;Rückruf +1003;Zoe;Fischer;München;Neukunde +1004;Lena;Wagner;Köln;Bestandskunde diff --git a/Installer.cs b/Installer.cs new file mode 100644 index 0000000..1aa050d --- /dev/null +++ b/Installer.cs @@ -0,0 +1,371 @@ +using System; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Windows.Forms; +using Microsoft.Win32; + +[assembly: AssemblyTitle("PANDA Setup")] +[assembly: AssemblyDescription("Installer für PANDA")] +[assembly: AssemblyProduct("PANDA")] +[assembly: AssemblyCompany("PANDA")] +[assembly: AssemblyVersion("1.3.0.0")] +[assembly: AssemblyFileVersion("1.3.0.0")] + +namespace PandaSetup +{ + internal static class Program + { + [STAThread] + private static void Main(string[] args) + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + + if (args.Length == 1 && string.Equals(args[0], "--verify", StringComparison.OrdinalIgnoreCase)) + { + try + { + ResourcePayload.Read("PANDA.Application.exe"); + ResourcePayload.Read("PANDA.Uninstaller.exe"); + Environment.ExitCode = 0; + } + catch + { + Environment.ExitCode = 2; + } + return; + } + + if (args.Length == 2 && string.Equals(args[0], "--screenshot", StringComparison.OrdinalIgnoreCase)) + { + using (var form = new SetupForm()) + { + form.Show(); + Application.DoEvents(); + using (var bitmap = new Bitmap(form.Width, form.Height)) + { + form.DrawToBitmap(bitmap, new Rectangle(Point.Empty, form.Size)); + bitmap.Save(args[1], System.Drawing.Imaging.ImageFormat.Png); + } + form.Close(); + } + return; + } + + Application.Run(new SetupForm()); + } + } + + internal static class ResourcePayload + { + public static byte[] Read(string name) + { + using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(name)) + { + if (stream == null) + throw new InvalidDataException("Installationsdatei fehlt: " + name); + var bytes = new byte[stream.Length]; + int offset = 0; + while (offset < bytes.Length) + { + int read = stream.Read(bytes, offset, bytes.Length - offset); + if (read == 0) break; + offset += read; + } + if (offset != bytes.Length || bytes.Length < 1024) + throw new InvalidDataException("Installationsdatei ist unvollständig: " + name); + return bytes; + } + } + } + + internal sealed class SetupForm : Form + { + private readonly Color Navy = Color.FromArgb(24, 38, 58); + private readonly Color Blue = Color.FromArgb(41, 112, 255); + private readonly Color Background = Color.FromArgb(244, 247, 251); + private readonly Color Muted = Color.FromArgb(94, 108, 128); + private readonly TextBox installPath = new TextBox(); + private readonly CheckBox desktopShortcut = new CheckBox(); + private readonly CheckBox startMenuShortcut = new CheckBox(); + private readonly CheckBox launchAfterInstall = new CheckBox(); + private readonly Button installButton = new Button(); + private readonly Label statusLabel = new Label(); + + public SetupForm() + { + Text = "PANDA Setup"; + StartPosition = FormStartPosition.CenterScreen; + FormBorderStyle = FormBorderStyle.FixedDialog; + MaximizeBox = false; + MinimizeBox = false; + ClientSize = new Size(720, 520); + BackColor = Background; + Font = new Font("Segoe UI", 9F); + Icon = System.Drawing.Icon.ExtractAssociatedIcon(Application.ExecutablePath) ?? SystemIcons.Application; + BuildLayout(); + } + + private void BuildLayout() + { + var root = new TableLayoutPanel + { + Dock = DockStyle.Fill, + ColumnCount = 1, + RowCount = 4, + Padding = new Padding(30, 24, 30, 22), + BackColor = Background + }; + root.RowStyles.Add(new RowStyle(SizeType.Absolute, 100)); + root.RowStyles.Add(new RowStyle(SizeType.Absolute, 120)); + root.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); + root.RowStyles.Add(new RowStyle(SizeType.Absolute, 74)); + Controls.Add(root); + + var heading = new Panel { Dock = DockStyle.Fill }; + heading.Controls.Add(new Label + { + Text = "PANDA installieren", + Font = new Font("Segoe UI Semibold", 22F), + ForeColor = Navy, + AutoSize = true, + Location = new Point(0, 0) + }); + heading.Controls.Add(new Label + { + Text = "Pseudonymisierung alphanumerischer Nutzdaten durch Alphabetverschiebung", + ForeColor = Muted, + AutoSize = true, + Location = new Point(2, 48) + }); + heading.Controls.Add(new Label + { + Text = "Version 1.3.0 • Installation für den aktuellen Windows-Benutzer", + ForeColor = Blue, + AutoSize = true, + Location = new Point(2, 73) + }); + root.Controls.Add(heading, 0, 0); + + var location = new TableLayoutPanel + { + Dock = DockStyle.Fill, + ColumnCount = 2, + RowCount = 3, + BackColor = Color.White, + Padding = new Padding(18, 14, 18, 12), + Margin = new Padding(0, 0, 0, 14) + }; + location.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); + location.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110)); + location.RowStyles.Add(new RowStyle(SizeType.Absolute, 26)); + location.RowStyles.Add(new RowStyle(SizeType.Absolute, 34)); + location.RowStyles.Add(new RowStyle(SizeType.Absolute, 26)); + var locationTitle = new Label + { + Text = "INSTALLATIONSORDNER", + Font = new Font("Segoe UI Semibold", 9F), + ForeColor = Navy, + AutoSize = true, + Anchor = AnchorStyles.Left + }; + location.Controls.Add(locationTitle, 0, 0); + location.SetColumnSpan(locationTitle, 2); + installPath.Text = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", "PANDA"); + installPath.Dock = DockStyle.Fill; + installPath.ForeColor = Navy; + location.Controls.Add(installPath, 0, 1); + var browse = new Button + { + Text = "Durchsuchen …", + Anchor = AnchorStyles.None, + Size = new Size(102, 28), + FlatStyle = FlatStyle.Flat, + BackColor = Color.White, + ForeColor = Navy, + Margin = new Padding(8, 0, 0, 0) + }; + browse.FlatAppearance.BorderColor = Color.FromArgb(206, 216, 230); + browse.Click += delegate { BrowseForFolder(); }; + location.Controls.Add(browse, 1, 1); + var note = new Label + { + Text = "Keine Administratorrechte erforderlich.", + ForeColor = Muted, + AutoSize = true, + Anchor = AnchorStyles.Left + }; + location.Controls.Add(note, 0, 2); + location.SetColumnSpan(note, 2); + root.Controls.Add(location, 0, 1); + + var options = new Panel { Dock = DockStyle.Fill, BackColor = Color.White, Padding = new Padding(18, 14, 18, 12) }; + options.Controls.Add(new Label + { + Text = "OPTIONEN", + Font = new Font("Segoe UI Semibold", 9F), + ForeColor = Navy, + AutoSize = true, + Location = new Point(18, 16) + }); + desktopShortcut.Text = "Desktop-Verknüpfung erstellen"; + desktopShortcut.Checked = true; + desktopShortcut.AutoSize = true; + desktopShortcut.ForeColor = Navy; + desktopShortcut.Location = new Point(20, 52); + startMenuShortcut.Text = "Eintrag im Startmenü erstellen"; + startMenuShortcut.Checked = true; + startMenuShortcut.AutoSize = true; + startMenuShortcut.ForeColor = Navy; + startMenuShortcut.Location = new Point(20, 82); + launchAfterInstall.Text = "PANDA nach der Installation starten"; + launchAfterInstall.Checked = true; + launchAfterInstall.AutoSize = true; + launchAfterInstall.ForeColor = Navy; + launchAfterInstall.Location = new Point(20, 112); + options.Controls.Add(desktopShortcut); + options.Controls.Add(startMenuShortcut); + options.Controls.Add(launchAfterInstall); + root.Controls.Add(options, 0, 2); + + var footer = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 2, RowCount = 1, Padding = new Padding(0, 18, 0, 0) }; + footer.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); + footer.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 190)); + footer.RowStyles.Add(new RowStyle(SizeType.Absolute, 38)); + statusLabel.Text = "Bereit zur Installation."; + statusLabel.ForeColor = Muted; + statusLabel.Dock = DockStyle.Fill; + statusLabel.TextAlign = ContentAlignment.MiddleLeft; + footer.Controls.Add(statusLabel, 0, 0); + installButton.Text = "Jetzt installieren"; + installButton.Dock = DockStyle.Fill; + installButton.Margin = new Padding(0); + installButton.FlatStyle = FlatStyle.Flat; + installButton.FlatAppearance.BorderSize = 0; + installButton.BackColor = Blue; + installButton.ForeColor = Color.White; + installButton.Cursor = Cursors.Hand; + installButton.Click += delegate { Install(); }; + footer.Controls.Add(installButton, 1, 0); + root.Controls.Add(footer, 0, 3); + AcceptButton = installButton; + } + + private void BrowseForFolder() + { + using (var dialog = new FolderBrowserDialog()) + { + dialog.Description = "Installationsordner für PANDA auswählen"; + dialog.SelectedPath = installPath.Text; + if (dialog.ShowDialog(this) == DialogResult.OK) + installPath.Text = Path.Combine(dialog.SelectedPath, "PANDA"); + } + } + + private void Install() + { + string target; + try + { + target = Path.GetFullPath(installPath.Text.Trim()); + if (string.Equals(target, Path.GetPathRoot(target), StringComparison.OrdinalIgnoreCase)) + throw new InvalidOperationException("Bitte wähle keinen Laufwerks-Stammordner aus."); + + installButton.Enabled = false; + statusLabel.Text = "PANDA wird installiert …"; + Cursor = Cursors.WaitCursor; + Application.DoEvents(); + + Directory.CreateDirectory(target); + string applicationPath = Path.Combine(target, "PANDA.exe"); + string uninstallerPath = Path.Combine(target, "PANDA-Uninstall.exe"); + File.WriteAllBytes(applicationPath, ResourcePayload.Read("PANDA.Application.exe")); + File.WriteAllBytes(uninstallerPath, ResourcePayload.Read("PANDA.Uninstaller.exe")); + + string desktopLink = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), "PANDA.lnk"); + if (desktopShortcut.Checked) + Shortcut.Create(desktopLink, applicationPath, target, "PANDA starten"); + else if (File.Exists(desktopLink)) + File.Delete(desktopLink); + + string startMenuFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Programs), "PANDA"); + if (startMenuShortcut.Checked) + { + Directory.CreateDirectory(startMenuFolder); + Shortcut.Create(Path.Combine(startMenuFolder, "PANDA.lnk"), applicationPath, target, "PANDA starten"); + Shortcut.Create(Path.Combine(startMenuFolder, "PANDA deinstallieren.lnk"), uninstallerPath, target, "PANDA deinstallieren"); + } + else if (Directory.Exists(startMenuFolder)) + { + string oldApplicationLink = Path.Combine(startMenuFolder, "PANDA.lnk"); + string oldUninstallLink = Path.Combine(startMenuFolder, "PANDA deinstallieren.lnk"); + if (File.Exists(oldApplicationLink)) File.Delete(oldApplicationLink); + if (File.Exists(oldUninstallLink)) File.Delete(oldUninstallLink); + if (Directory.GetFileSystemEntries(startMenuFolder).Length == 0) Directory.Delete(startMenuFolder, false); + } + + RegisterUninstaller(target, applicationPath, uninstallerPath); + statusLabel.Text = "Installation erfolgreich abgeschlossen."; + Cursor = Cursors.Default; + MessageBox.Show(this, "PANDA wurde erfolgreich installiert.\r\n\r\nDie Deinstallation ist über Windows › Installierte Apps ‹ oder das Startmenü möglich.", "PANDA installiert", MessageBoxButtons.OK, MessageBoxIcon.Information); + if (launchAfterInstall.Checked) + Process.Start(applicationPath); + Close(); + } + catch (Exception exception) + { + Cursor = Cursors.Default; + installButton.Enabled = true; + statusLabel.Text = "Installation fehlgeschlagen."; + MessageBox.Show(this, "PANDA konnte nicht installiert werden.\r\n\r\n" + exception.Message + "\r\n\r\nFalls PANDA bereits läuft, schließe das Programm und versuche es erneut.", "Installationsfehler", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + private static void RegisterUninstaller(string target, string applicationPath, string uninstallerPath) + { + using (RegistryKey key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\PANDA")) + { + key.SetValue("DisplayName", "PANDA"); + key.SetValue("DisplayVersion", "1.3.0"); + key.SetValue("DisplayIcon", applicationPath); + key.SetValue("Publisher", "PANDA"); + key.SetValue("InstallLocation", target); + key.SetValue("UninstallString", "\"" + uninstallerPath + "\""); + key.SetValue("QuietUninstallString", "\"" + uninstallerPath + "\" --silent"); + key.SetValue("NoModify", 1, RegistryValueKind.DWord); + key.SetValue("NoRepair", 1, RegistryValueKind.DWord); + key.SetValue("EstimatedSize", 100, RegistryValueKind.DWord); + } + } + } + + internal static class Shortcut + { + public static void Create(string shortcutPath, string targetPath, string workingDirectory, string description) + { + Type shellType = Type.GetTypeFromProgID("WScript.Shell"); + if (shellType == null) + throw new InvalidOperationException("Windows-Verknüpfungen werden auf diesem System nicht unterstützt."); + object shell = Activator.CreateInstance(shellType); + object shortcut = null; + try + { + shortcut = shellType.InvokeMember("CreateShortcut", BindingFlags.InvokeMethod, null, shell, new object[] { shortcutPath }); + Type shortcutType = shortcut.GetType(); + shortcutType.InvokeMember("TargetPath", BindingFlags.SetProperty, null, shortcut, new object[] { targetPath }); + shortcutType.InvokeMember("WorkingDirectory", BindingFlags.SetProperty, null, shortcut, new object[] { workingDirectory }); + shortcutType.InvokeMember("Description", BindingFlags.SetProperty, null, shortcut, new object[] { description }); + shortcutType.InvokeMember("IconLocation", BindingFlags.SetProperty, null, shortcut, new object[] { targetPath + ",0" }); + shortcutType.InvokeMember("Save", BindingFlags.InvokeMethod, null, shortcut, null); + } + finally + { + if (shortcut != null && Marshal.IsComObject(shortcut)) Marshal.FinalReleaseComObject(shortcut); + if (shell != null && Marshal.IsComObject(shell)) Marshal.FinalReleaseComObject(shell); + } + } + } +} diff --git a/PANDA-icon-final.png b/PANDA-icon-final.png new file mode 100644 index 0000000..49da314 Binary files /dev/null and b/PANDA-icon-final.png differ diff --git a/PANDA.ico b/PANDA.ico new file mode 100644 index 0000000..8f0abd4 Binary files /dev/null and b/PANDA.ico differ diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..118d853 --- /dev/null +++ b/Program.cs @@ -0,0 +1,1250 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Windows.Forms; + +[assembly: AssemblyTitle("PANDA")] +[assembly: AssemblyDescription("Pseudonymisierung alphanumerischer Nutzdaten durch Alphabetverschiebung")] +[assembly: AssemblyProduct("PANDA")] +[assembly: AssemblyCompany("PANDA")] +[assembly: AssemblyVersion("1.3.0.0")] +[assembly: AssemblyFileVersion("1.3.0.0")] + +namespace Panda +{ + internal static class Program + { + [STAThread] + private static void Main(string[] args) + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + if (args.Length == 2 && string.Equals(args[0], "--screenshot", StringComparison.OrdinalIgnoreCase)) + { + using (var form = new MainForm()) + { + form.Size = new Size(1320, 820); + form.Show(); + form.LoadPreviewData(); + Application.DoEvents(); + using (var bitmap = new Bitmap(form.Width, form.Height)) + { + form.DrawToBitmap(bitmap, new Rectangle(Point.Empty, form.Size)); + bitmap.Save(args[1], System.Drawing.Imaging.ImageFormat.Png); + } + form.Close(); + } + return; + } + if (args.Length == 3 && string.Equals(args[0], "--wizard-screenshot", StringComparison.OrdinalIgnoreCase)) + { + using (var wizard = new ImportWizardForm(args[1])) + { + wizard.Show(); + Application.DoEvents(); + wizard.UncheckLastColumnForPreview(); + Application.DoEvents(); + using (var bitmap = new Bitmap(wizard.Width, wizard.Height)) + { + wizard.DrawToBitmap(bitmap, new Rectangle(Point.Empty, wizard.Size)); + bitmap.Save(args[2], System.Drawing.Imaging.ImageFormat.Png); + } + wizard.Close(); + } + return; + } + Application.Run(new MainForm()); + } + } + + internal sealed class CsvDocument + { + public List Headers = new List(); + public List> Rows = new List>(); + public char Delimiter; + public bool FirstRowIsHeader; + } + + internal static class CsvCodec + { + public static CsvDocument Load(string path, bool firstRowIsHeader) + { + string text; + using (var reader = new StreamReader(path, Encoding.UTF8, true)) + text = reader.ReadToEnd(); + + char delimiter = DetectDelimiter(text); + var records = Parse(text, delimiter); + var document = new CsvDocument + { + Delimiter = delimiter, + FirstRowIsHeader = firstRowIsHeader + }; + + int columnCount = records.Count == 0 ? 0 : records.Max(row => row.Count); + if (firstRowIsHeader && records.Count > 0) + { + for (int column = 0; column < columnCount; column++) + { + string value = column < records[0].Count ? records[0][column] : string.Empty; + document.Headers.Add(string.IsNullOrWhiteSpace(value) ? "Spalte " + (column + 1) : value); + } + records.RemoveAt(0); + } + else + { + for (int column = 0; column < columnCount; column++) + document.Headers.Add("Spalte " + (column + 1)); + } + + foreach (var record in records) + { + while (record.Count < columnCount) + record.Add(string.Empty); + document.Rows.Add(record); + } + + return document; + } + + public static void Save(string path, CsvDocument document, IList> rows) + { + using (var writer = new StreamWriter(path, false, new UTF8Encoding(true))) + { + if (document.FirstRowIsHeader) + WriteRecord(writer, document.Headers, document.Delimiter); + + foreach (var row in rows) + WriteRecord(writer, row, document.Delimiter); + } + } + + public static CsvDocument SelectColumns(CsvDocument source, IList selectedColumns) + { + var result = new CsvDocument + { + Delimiter = source.Delimiter, + FirstRowIsHeader = source.FirstRowIsHeader + }; + + foreach (int column in selectedColumns) + { + if (column < 0 || column >= source.Headers.Count) + throw new ArgumentOutOfRangeException("selectedColumns"); + result.Headers.Add(source.Headers[column]); + } + + foreach (var sourceRow in source.Rows) + { + var row = new List(); + foreach (int column in selectedColumns) + row.Add(column < sourceRow.Count ? sourceRow[column] : string.Empty); + result.Rows.Add(row); + } + return result; + } + + private static void WriteRecord(TextWriter writer, IEnumerable values, char delimiter) + { + writer.WriteLine(string.Join(delimiter.ToString(), values.Select(value => Escape(value ?? string.Empty, delimiter)))); + } + + private static string Escape(string value, char delimiter) + { + if (value.IndexOfAny(new[] { delimiter, '"', '\r', '\n' }) < 0) + return value; + return "\"" + value.Replace("\"", "\"\"") + "\""; + } + + internal static char DetectDelimiter(string text) + { + string firstRecord = GetFirstLogicalRecord(text); + char[] candidates = { ';', ',', '\t' }; + char best = ';'; + int bestCount = -1; + foreach (char candidate in candidates) + { + int count = CountOutsideQuotes(firstRecord, candidate); + if (count > bestCount) + { + bestCount = count; + best = candidate; + } + } + return best; + } + + private static string GetFirstLogicalRecord(string text) + { + var builder = new StringBuilder(); + bool quoted = false; + for (int index = 0; index < text.Length; index++) + { + char current = text[index]; + if (current == '"') + { + if (quoted && index + 1 < text.Length && text[index + 1] == '"') + { + builder.Append("\"\""); + index++; + continue; + } + quoted = !quoted; + } + if (!quoted && (current == '\r' || current == '\n')) + break; + builder.Append(current); + } + return builder.ToString(); + } + + private static int CountOutsideQuotes(string text, char delimiter) + { + bool quoted = false; + int count = 0; + for (int index = 0; index < text.Length; index++) + { + if (text[index] == '"') + { + if (quoted && index + 1 < text.Length && text[index + 1] == '"') + { + index++; + continue; + } + quoted = !quoted; + } + else if (!quoted && text[index] == delimiter) + { + count++; + } + } + return count; + } + + internal static List> Parse(string text, char delimiter) + { + var rows = new List>(); + var row = new List(); + var field = new StringBuilder(); + bool quoted = false; + + for (int index = 0; index < text.Length; index++) + { + char current = text[index]; + if (quoted) + { + if (current == '"') + { + if (index + 1 < text.Length && text[index + 1] == '"') + { + field.Append('"'); + index++; + } + else + { + quoted = false; + } + } + else + { + field.Append(current); + } + } + else if (current == '"' && field.Length == 0) + { + quoted = true; + } + else if (current == delimiter) + { + row.Add(field.ToString()); + field.Clear(); + } + else if (current == '\r' || current == '\n') + { + if (current == '\r' && index + 1 < text.Length && text[index + 1] == '\n') + index++; + row.Add(field.ToString()); + field.Clear(); + rows.Add(row); + row = new List(); + } + else + { + field.Append(current); + } + } + + if (field.Length > 0 || row.Count > 0) + { + row.Add(field.ToString()); + rows.Add(row); + } + return rows; + } + } + + internal static class LetterShifter + { + public static string Shift(string value, int amount) + { + if (string.IsNullOrEmpty(value) || amount == 0) + return value; + + var result = new StringBuilder(value.Length); + foreach (char character in value) + { + if (character >= 'A' && character <= 'Z') + result.Append(ShiftInRange(character, 'A', 26, amount)); + else if (character >= 'a' && character <= 'z') + result.Append(ShiftInRange(character, 'a', 26, amount)); + else + result.Append(character); + } + return result.ToString(); + } + + private static char ShiftInRange(char value, char start, int length, int amount) + { + int shifted = ((value - start + amount) % length + length) % length; + return (char)(start + shifted); + } + } + + internal sealed class ImportWizardForm : Form + { + private readonly string csvPath; + private readonly Color Navy = Color.FromArgb(24, 38, 58); + private readonly Color Blue = Color.FromArgb(41, 112, 255); + private readonly Color Background = Color.FromArgb(244, 247, 251); + private readonly Color Muted = Color.FromArgb(94, 108, 128); + private readonly CheckBox headerCheckBox = new CheckBox(); + private readonly Label formatLabel = new Label(); + private readonly Label selectionLabel = new Label(); + private readonly CheckedListBox columnList = new CheckedListBox(); + private readonly DataGridView previewGrid = new DataGridView(); + private readonly Button importButton = new Button(); + private CsvDocument loadedDocument; + + public CsvDocument SelectedDocument { get; private set; } + + public ImportWizardForm(string path) + { + csvPath = path; + Text = "PANDA Import-Assistent"; + StartPosition = FormStartPosition.CenterParent; + MinimumSize = new Size(900, 600); + Size = new Size(1040, 680); + BackColor = Background; + Font = new Font("Segoe UI", 9F); + Icon = System.Drawing.Icon.ExtractAssociatedIcon(Application.ExecutablePath) ?? SystemIcons.Application; + BuildLayout(); + ReloadPreview(true); + } + + private void BuildLayout() + { + var root = new TableLayoutPanel + { + Dock = DockStyle.Fill, + ColumnCount = 1, + RowCount = 4, + Padding = new Padding(22, 18, 22, 18), + BackColor = Background + }; + root.RowStyles.Add(new RowStyle(SizeType.Absolute, 72)); + root.RowStyles.Add(new RowStyle(SizeType.Absolute, 72)); + root.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); + root.RowStyles.Add(new RowStyle(SizeType.Absolute, 58)); + Controls.Add(root); + + var titlePanel = new Panel { Dock = DockStyle.Fill }; + titlePanel.Controls.Add(new Label + { + Text = "CSV-Import vorbereiten", + Font = new Font("Segoe UI Semibold", 18F), + ForeColor = Navy, + AutoSize = true, + Location = new Point(0, 0) + }); + titlePanel.Controls.Add(new Label + { + Text = "Prüfe das Format und wähle die Spalten aus, die PANDA übernehmen soll.", + ForeColor = Muted, + AutoSize = true, + Location = new Point(2, 39) + }); + root.Controls.Add(titlePanel, 0, 0); + + var filePanel = new TableLayoutPanel + { + Dock = DockStyle.Fill, + ColumnCount = 3, + RowCount = 2, + BackColor = Color.White, + Padding = new Padding(14, 8, 14, 8), + Margin = new Padding(0, 0, 0, 12) + }; + filePanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 55)); + filePanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 240)); + filePanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 45)); + filePanel.RowStyles.Add(new RowStyle(SizeType.Absolute, 24)); + filePanel.RowStyles.Add(new RowStyle(SizeType.Absolute, 24)); + var stepOne = new Label + { + Text = "1 DATEI UND FORMAT", + Font = new Font("Segoe UI Semibold", 9F), + ForeColor = Blue, + AutoSize = true, + Anchor = AnchorStyles.Left + }; + filePanel.Controls.Add(stepOne, 0, 0); + filePanel.SetColumnSpan(stepOne, 3); + filePanel.Controls.Add(new Label + { + Text = Path.GetFileName(csvPath), + ForeColor = Navy, + AutoEllipsis = true, + Dock = DockStyle.Fill, + TextAlign = ContentAlignment.MiddleLeft + }, 0, 1); + headerCheckBox.Text = "Erste Zeile enthält Überschriften"; + headerCheckBox.Checked = true; + headerCheckBox.AutoSize = true; + headerCheckBox.ForeColor = Navy; + headerCheckBox.Anchor = AnchorStyles.Left; + headerCheckBox.CheckedChanged += delegate { ReloadPreview(false); }; + filePanel.Controls.Add(headerCheckBox, 1, 1); + formatLabel.ForeColor = Muted; + formatLabel.AutoEllipsis = true; + formatLabel.Dock = DockStyle.Fill; + formatLabel.TextAlign = ContentAlignment.MiddleRight; + filePanel.Controls.Add(formatLabel, 2, 1); + root.Controls.Add(filePanel, 0, 1); + + var content = new TableLayoutPanel + { + Dock = DockStyle.Fill, + ColumnCount = 2, + RowCount = 2, + BackColor = Background, + Margin = new Padding(0) + }; + content.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 280)); + content.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); + content.RowStyles.Add(new RowStyle(SizeType.Absolute, 42)); + content.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); + content.Controls.Add(CreateSectionHeader("2 SPALTEN AUSWÄHLEN", false), 0, 0); + content.Controls.Add(CreateSectionHeader("VORSCHAU DER DATEI", true), 1, 0); + root.Controls.Add(content, 0, 2); + + var selectionPanel = new TableLayoutPanel + { + Dock = DockStyle.Fill, + ColumnCount = 2, + RowCount = 3, + BackColor = Color.White, + Padding = new Padding(12, 10, 12, 12), + Margin = new Padding(0, 0, 7, 0) + }; + selectionPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50)); + selectionPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50)); + selectionPanel.RowStyles.Add(new RowStyle(SizeType.Absolute, 30)); + selectionPanel.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); + selectionPanel.RowStyles.Add(new RowStyle(SizeType.Absolute, 26)); + var allButton = CreateLinkButton("Alle auswählen"); + allButton.Click += delegate { SetAllColumns(true); }; + var noneButton = CreateLinkButton("Auswahl aufheben"); + noneButton.Click += delegate { SetAllColumns(false); }; + selectionPanel.Controls.Add(allButton, 0, 0); + selectionPanel.Controls.Add(noneButton, 1, 0); + columnList.Dock = DockStyle.Fill; + columnList.BorderStyle = BorderStyle.None; + columnList.CheckOnClick = true; + columnList.ForeColor = Navy; + columnList.BackColor = Color.White; + columnList.HorizontalScrollbar = true; + columnList.ItemCheck += delegate(object sender, ItemCheckEventArgs args) + { + if (args.Index >= 0 && args.Index < previewGrid.Columns.Count) + previewGrid.Columns[args.Index].Visible = args.NewValue == CheckState.Checked; + if (IsHandleCreated) + BeginInvoke(new Action(UpdateSelectionStatus)); + }; + selectionPanel.SetColumnSpan(columnList, 2); + selectionPanel.Controls.Add(columnList, 0, 1); + selectionLabel.ForeColor = Muted; + selectionLabel.Dock = DockStyle.Fill; + selectionLabel.TextAlign = ContentAlignment.MiddleLeft; + selectionPanel.SetColumnSpan(selectionLabel, 2); + selectionPanel.Controls.Add(selectionLabel, 0, 2); + content.Controls.Add(selectionPanel, 0, 1); + + ConfigurePreviewGrid(); + content.Controls.Add(previewGrid, 1, 1); + + var footer = new TableLayoutPanel + { + Dock = DockStyle.Fill, + ColumnCount = 4, + RowCount = 1, + Padding = new Padding(0, 12, 0, 0) + }; + footer.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); + footer.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 140)); + footer.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 10)); + footer.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 140)); + footer.RowStyles.Add(new RowStyle(SizeType.Absolute, 36)); + importButton.Text = "Importieren"; + importButton.Dock = DockStyle.Fill; + importButton.Margin = new Padding(0); + importButton.FlatStyle = FlatStyle.Flat; + importButton.FlatAppearance.BorderSize = 0; + importButton.BackColor = Blue; + importButton.ForeColor = Color.White; + importButton.Cursor = Cursors.Hand; + importButton.Click += delegate { FinishImport(); }; + var cancelButton = new Button + { + Text = "Abbrechen", + DialogResult = DialogResult.Cancel, + Dock = DockStyle.Fill, + FlatStyle = FlatStyle.Flat, + BackColor = Color.White, + ForeColor = Navy, + Margin = new Padding(0) + }; + cancelButton.FlatAppearance.BorderColor = Color.FromArgb(206, 216, 230); + footer.Controls.Add(cancelButton, 1, 0); + footer.Controls.Add(importButton, 3, 0); + root.Controls.Add(footer, 0, 3); + AcceptButton = importButton; + CancelButton = cancelButton; + } + + private Panel CreateSectionHeader(string text, bool preview) + { + var panel = new Panel + { + Dock = DockStyle.Fill, + BackColor = preview ? Color.FromArgb(236, 243, 255) : Color.White, + Margin = preview ? new Padding(7, 0, 0, 0) : new Padding(0, 0, 7, 0) + }; + panel.Controls.Add(new Label + { + Text = text, + Font = new Font("Segoe UI Semibold", 9F), + ForeColor = preview ? Blue : Navy, + AutoSize = true, + Location = new Point(12, 12) + }); + return panel; + } + + private Button CreateLinkButton(string text) + { + var button = new Button + { + Text = text, + Dock = DockStyle.Fill, + FlatStyle = FlatStyle.Flat, + BackColor = Color.White, + ForeColor = Blue, + Cursor = Cursors.Hand, + Margin = new Padding(0, 0, 4, 4) + }; + button.FlatAppearance.BorderSize = 0; + return button; + } + + private void ConfigurePreviewGrid() + { + previewGrid.Dock = DockStyle.Fill; + previewGrid.Margin = new Padding(7, 0, 0, 0); + previewGrid.BackgroundColor = Color.White; + previewGrid.BorderStyle = BorderStyle.None; + previewGrid.CellBorderStyle = DataGridViewCellBorderStyle.SingleHorizontal; + previewGrid.GridColor = Color.FromArgb(228, 234, 242); + previewGrid.RowHeadersVisible = false; + previewGrid.ColumnHeadersHeight = 36; + previewGrid.ColumnHeadersDefaultCellStyle.BackColor = Color.FromArgb(248, 250, 253); + previewGrid.ColumnHeadersDefaultCellStyle.ForeColor = Navy; + previewGrid.ColumnHeadersDefaultCellStyle.Font = new Font("Segoe UI Semibold", 9F); + previewGrid.EnableHeadersVisualStyles = false; + previewGrid.DefaultCellStyle.BackColor = Color.White; + previewGrid.DefaultCellStyle.ForeColor = Navy; + previewGrid.DefaultCellStyle.SelectionBackColor = Color.White; + previewGrid.DefaultCellStyle.SelectionForeColor = Navy; + previewGrid.RowTemplate.Height = 28; + previewGrid.AllowUserToAddRows = false; + previewGrid.AllowUserToDeleteRows = false; + previewGrid.AllowUserToOrderColumns = false; + previewGrid.ReadOnly = true; + previewGrid.MultiSelect = false; + previewGrid.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill; + } + + private void ReloadPreview(bool firstLoad) + { + try + { + loadedDocument = CsvCodec.Load(csvPath, headerCheckBox.Checked); + if (loadedDocument.Headers.Count == 0) + throw new InvalidDataException("Die Datei enthält keine auswertbaren CSV-Daten."); + formatLabel.Text = loadedDocument.Rows.Count + " Zeilen • " + loadedDocument.Headers.Count + " Spalten • " + DelimiterName(loadedDocument.Delimiter); + columnList.Items.Clear(); + foreach (string header in loadedDocument.Headers) + columnList.Items.Add(header, true); + PopulatePreview(); + UpdateSelectionStatus(); + } + catch (Exception exception) + { + if (!firstLoad) + MessageBox.Show(this, "Die Vorschau konnte nicht aktualisiert werden.\r\n\r\n" + exception.Message, "Import-Assistent", MessageBoxButtons.OK, MessageBoxIcon.Error); + importButton.Enabled = false; + } + } + + private void PopulatePreview() + { + previewGrid.SuspendLayout(); + previewGrid.Columns.Clear(); + previewGrid.Rows.Clear(); + for (int column = 0; column < loadedDocument.Headers.Count; column++) + { + previewGrid.Columns.Add("Preview" + column, loadedDocument.Headers[column]); + previewGrid.Columns[column].SortMode = DataGridViewColumnSortMode.NotSortable; + } + foreach (var row in loadedDocument.Rows.Take(50)) + previewGrid.Rows.Add(row.Cast().ToArray()); + previewGrid.ClearSelection(); + previewGrid.ResumeLayout(); + } + + private void SetAllColumns(bool selected) + { + for (int index = 0; index < columnList.Items.Count; index++) + columnList.SetItemChecked(index, selected); + BeginInvoke(new Action(UpdateSelectionStatus)); + } + + private void UpdateSelectionStatus() + { + int count = columnList.CheckedIndices.Count; + selectionLabel.Text = count + " von " + columnList.Items.Count + " Spalten ausgewählt"; + importButton.Enabled = count > 0; + } + + private void FinishImport() + { + var selected = columnList.CheckedIndices.Cast().ToList(); + if (selected.Count == 0) + { + MessageBox.Show(this, "Bitte wähle mindestens eine Spalte aus.", "Keine Spalte ausgewählt", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + SelectedDocument = CsvCodec.SelectColumns(loadedDocument, selected); + DialogResult = DialogResult.OK; + Close(); + } + + private static string DelimiterName(char delimiter) + { + if (delimiter == ';') return "Semikolon"; + if (delimiter == ',') return "Komma"; + if (delimiter == '\t') return "Tabulator"; + return delimiter.ToString(); + } + + internal void UncheckLastColumnForPreview() + { + if (columnList.Items.Count > 1) + columnList.SetItemChecked(columnList.Items.Count - 1, false); + } + } + + internal sealed class MainForm : Form + { + private readonly Color Navy = Color.FromArgb(24, 38, 58); + private readonly Color Blue = Color.FromArgb(41, 112, 255); + private readonly Color PaleBlue = Color.FromArgb(236, 243, 255); + private readonly Color Background = Color.FromArgb(244, 247, 251); + private readonly Color Muted = Color.FromArgb(94, 108, 128); + + private readonly DataGridView originalGrid = new DataGridView(); + private readonly DataGridView resultGrid = new DataGridView(); + private readonly ComboBox scopeComboBox = new ComboBox(); + private readonly NumericUpDown stepNumeric = new NumericUpDown(); + private readonly Label statusLabel = new Label(); + private readonly Label fileLabel = new Label(); + private readonly Button exportButton = new Button(); + private readonly Button resetButton = new Button(); + + private CsvDocument document; + private string importedPath; + + public MainForm() + { + Text = "PANDA – Pseudonymisierung alphanumerischer Nutzdaten"; + StartPosition = FormStartPosition.CenterScreen; + MinimumSize = new Size(1050, 680); + Size = new Size(1320, 820); + BackColor = Background; + Font = new Font("Segoe UI", 9F); + Icon = System.Drawing.Icon.ExtractAssociatedIcon(Application.ExecutablePath) ?? SystemIcons.Application; + + BuildLayout(); + ConfigureGrid(originalGrid, true); + ConfigureGrid(resultGrid, false); + SetDocumentControlsEnabled(false); + } + + private void BuildLayout() + { + var root = new TableLayoutPanel + { + Dock = DockStyle.Fill, + ColumnCount = 1, + RowCount = 4, + Padding = new Padding(20, 18, 20, 16), + BackColor = Background + }; + root.RowStyles.Add(new RowStyle(SizeType.Absolute, 68)); + root.RowStyles.Add(new RowStyle(SizeType.Absolute, 92)); + root.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); + root.RowStyles.Add(new RowStyle(SizeType.Absolute, 40)); + Controls.Add(root); + + var header = new Panel { Dock = DockStyle.Fill, BackColor = Background }; + var title = new Label + { + Text = "PANDA", + ForeColor = Navy, + Font = new Font("Segoe UI Semibold", 19F), + AutoSize = true, + Location = new Point(0, 0) + }; + var subtitle = new Label + { + Text = "Pseudonymisierung alphanumerischer Nutzdaten durch Alphabetverschiebung", + ForeColor = Muted, + AutoSize = true, + Location = new Point(2, 39) + }; + header.Controls.Add(title); + header.Controls.Add(subtitle); + root.Controls.Add(header, 0, 0); + + var toolbar = new TableLayoutPanel + { + Dock = DockStyle.Fill, + ColumnCount = 8, + RowCount = 2, + Padding = new Padding(12, 10, 12, 8), + BackColor = Color.White, + Margin = new Padding(0, 0, 0, 12) + }; + toolbar.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 148)); + toolbar.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 160)); + toolbar.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 76)); + toolbar.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 135)); + toolbar.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 135)); + toolbar.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 108)); + toolbar.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 122)); + toolbar.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); + toolbar.RowStyles.Add(new RowStyle(SizeType.Absolute, 34)); + toolbar.RowStyles.Add(new RowStyle(SizeType.Absolute, 28)); + root.Controls.Add(toolbar, 0, 1); + + var importButton = CreateButton("Importieren", Blue, Color.White); + importButton.Click += delegate { ImportCsv(); }; + toolbar.Controls.Add(importButton, 0, 0); + + scopeComboBox.DropDownStyle = ComboBoxStyle.DropDownList; + scopeComboBox.Items.AddRange(new object[] { "Markierte Zellen", "Aktuelle Zelle", "Alle Einträge" }); + scopeComboBox.SelectedIndex = 0; + scopeComboBox.Dock = DockStyle.Fill; + scopeComboBox.Margin = new Padding(7, 3, 7, 3); + toolbar.Controls.Add(scopeComboBox, 1, 0); + + stepNumeric.Minimum = 1; + stepNumeric.Maximum = 25; + stepNumeric.Value = 1; + stepNumeric.Dock = DockStyle.Fill; + stepNumeric.TextAlign = HorizontalAlignment.Center; + stepNumeric.Margin = new Padding(7, 3, 7, 3); + toolbar.Controls.Add(stepNumeric, 2, 0); + + var upButton = CreateButton("Hochzählen +", Color.FromArgb(29, 157, 105), Color.White); + upButton.Click += delegate { ApplyShift((int)stepNumeric.Value); }; + toolbar.Controls.Add(upButton, 3, 0); + + var downButton = CreateButton("Runterzählen −", Color.FromArgb(230, 91, 84), Color.White); + downButton.Click += delegate { ApplyShift(-(int)stepNumeric.Value); }; + toolbar.Controls.Add(downButton, 4, 0); + + resetButton.Text = "Zurücksetzen"; + StyleSecondaryButton(resetButton); + resetButton.Click += delegate { ResetResults(); }; + toolbar.Controls.Add(resetButton, 5, 0); + + exportButton.Text = "CSV exportieren"; + StyleSecondaryButton(exportButton); + exportButton.Click += delegate { ExportCsv(); }; + toolbar.Controls.Add(exportButton, 6, 0); + + fileLabel.Text = "Noch keine CSV geladen"; + fileLabel.ForeColor = Muted; + fileLabel.AutoEllipsis = true; + fileLabel.Dock = DockStyle.Fill; + fileLabel.TextAlign = ContentAlignment.MiddleLeft; + toolbar.SetColumnSpan(fileLabel, 4); + toolbar.Controls.Add(fileLabel, 0, 1); + + var hint = new Label + { + Text = "Tipp: Strg oder Umschalt gedrückt halten, um mehrere Zellen zu markieren.", + ForeColor = Muted, + AutoEllipsis = true, + Dock = DockStyle.Fill, + TextAlign = ContentAlignment.MiddleRight + }; + toolbar.SetColumnSpan(hint, 4); + toolbar.Controls.Add(hint, 4, 1); + + var grids = new TableLayoutPanel + { + Dock = DockStyle.Fill, + ColumnCount = 2, + RowCount = 2, + BackColor = Background, + Margin = new Padding(0) + }; + grids.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50)); + grids.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50)); + grids.RowStyles.Add(new RowStyle(SizeType.Absolute, 42)); + grids.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); + root.Controls.Add(grids, 0, 2); + + grids.Controls.Add(CreateGridHeader("ORIGINAL", "Importierte CSV-Werte", false), 0, 0); + grids.Controls.Add(CreateGridHeader("ERGEBNIS", "Veränderte Werte", true), 1, 0); + + originalGrid.Margin = new Padding(0, 0, 7, 0); + resultGrid.Margin = new Padding(7, 0, 0, 0); + grids.Controls.Add(originalGrid, 0, 1); + grids.Controls.Add(resultGrid, 1, 1); + + var statusPanel = new Panel { Dock = DockStyle.Fill, BackColor = Background }; + statusLabel.Text = "Bereit – bitte eine CSV-Datei importieren."; + statusLabel.ForeColor = Muted; + statusLabel.AutoSize = true; + statusLabel.Location = new Point(2, 12); + statusPanel.Controls.Add(statusLabel); + root.Controls.Add(statusPanel, 0, 3); + } + + private Panel CreateGridHeader(string eyebrow, string caption, bool result) + { + var panel = new Panel + { + Dock = DockStyle.Fill, + BackColor = result ? PaleBlue : Color.White, + Margin = result ? new Padding(7, 0, 0, 0) : new Padding(0, 0, 7, 0) + }; + var title = new Label + { + Text = eyebrow + " " + caption, + Font = new Font("Segoe UI Semibold", 10F), + ForeColor = result ? Blue : Navy, + AutoSize = true, + Location = new Point(12, 11) + }; + panel.Controls.Add(title); + return panel; + } + + private Button CreateButton(string text, Color background, Color foreground) + { + var button = new Button + { + Text = text, + Dock = DockStyle.Fill, + FlatStyle = FlatStyle.Flat, + BackColor = background, + ForeColor = foreground, + Cursor = Cursors.Hand, + Margin = new Padding(4, 2, 4, 2) + }; + button.FlatAppearance.BorderSize = 0; + return button; + } + + private void StyleSecondaryButton(Button button) + { + button.Dock = DockStyle.Fill; + button.FlatStyle = FlatStyle.Flat; + button.BackColor = Color.White; + button.ForeColor = Navy; + button.Cursor = Cursors.Hand; + button.Margin = new Padding(4, 2, 4, 2); + button.FlatAppearance.BorderColor = Color.FromArgb(206, 216, 230); + } + + private void ConfigureGrid(DataGridView grid, bool selectable) + { + grid.Dock = DockStyle.Fill; + grid.BackgroundColor = Color.White; + grid.BorderStyle = BorderStyle.None; + grid.CellBorderStyle = DataGridViewCellBorderStyle.SingleHorizontal; + grid.GridColor = Color.FromArgb(228, 234, 242); + grid.RowHeadersVisible = true; + grid.RowHeadersWidth = selectable ? 74 : 50; + grid.RowHeadersDefaultCellStyle.BackColor = Color.FromArgb(248, 250, 253); + grid.RowHeadersDefaultCellStyle.ForeColor = Muted; + grid.RowHeadersDefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter; + grid.ColumnHeadersHeight = 36; + grid.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.DisableResizing; + grid.ColumnHeadersDefaultCellStyle.BackColor = Color.FromArgb(248, 250, 253); + grid.ColumnHeadersDefaultCellStyle.ForeColor = Navy; + grid.ColumnHeadersDefaultCellStyle.Font = new Font("Segoe UI Semibold", 9F); + grid.ColumnHeadersDefaultCellStyle.SelectionBackColor = Color.FromArgb(248, 250, 253); + grid.EnableHeadersVisualStyles = false; + grid.DefaultCellStyle.BackColor = Color.White; + grid.DefaultCellStyle.ForeColor = Navy; + grid.DefaultCellStyle.SelectionBackColor = selectable ? Color.FromArgb(214, 227, 255) : Color.White; + grid.DefaultCellStyle.SelectionForeColor = Navy; + grid.DefaultCellStyle.Padding = new Padding(4, 2, 4, 2); + grid.RowTemplate.Height = 30; + grid.AllowUserToAddRows = false; + grid.AllowUserToDeleteRows = false; + grid.AllowUserToOrderColumns = false; + grid.AllowUserToResizeRows = false; + grid.MultiSelect = true; + grid.SelectionMode = DataGridViewSelectionMode.CellSelect; + grid.ReadOnly = true; + grid.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill; + grid.ClipboardCopyMode = DataGridViewClipboardCopyMode.EnableWithoutHeaderText; + grid.DataError += delegate { }; + + if (selectable) + { + grid.RowPostPaint += DrawOriginalRowHeader; + grid.RowHeaderMouseClick += ToggleOriginalRow; + grid.Scroll += delegate { SyncScroll(originalGrid, resultGrid); }; + } + else + { + grid.MultiSelect = false; + grid.TabStop = false; + grid.SelectionChanged += delegate + { + if (grid.SelectedCells.Count > 0) + grid.ClearSelection(); + }; + grid.CellMouseDown += delegate(object sender, DataGridViewCellMouseEventArgs args) + { + if (args.RowIndex < 0 || args.ColumnIndex < 0) + return; + originalGrid.ClearSelection(); + originalGrid.CurrentCell = originalGrid.Rows[args.RowIndex].Cells[args.ColumnIndex]; + originalGrid.CurrentCell.Selected = true; + originalGrid.Focus(); + BeginInvoke(new Action(delegate + { + grid.ClearSelection(); + grid.CurrentCell = null; + })); + }; + grid.Scroll += delegate { SyncScroll(resultGrid, originalGrid); }; + } + } + + private void DrawOriginalRowHeader(object sender, DataGridViewRowPostPaintEventArgs args) + { + var grid = (DataGridView)sender; + var bounds = new Rectangle(0, args.RowBounds.Top, grid.RowHeadersWidth, args.RowBounds.Height); + using (var backgroundBrush = new SolidBrush(Color.FromArgb(248, 250, 253))) + args.Graphics.FillRectangle(backgroundBrush, bounds); + using (var linePen = new Pen(Color.FromArgb(228, 234, 242))) + args.Graphics.DrawLine(linePen, bounds.Left, bounds.Bottom - 1, bounds.Right, bounds.Bottom - 1); + + bool selected = grid.Rows[args.RowIndex].Tag is bool && (bool)grid.Rows[args.RowIndex].Tag; + int checkSize = 14; + var checkPoint = new Point(8, bounds.Top + (bounds.Height - checkSize) / 2); + CheckBoxRenderer.DrawCheckBox(args.Graphics, checkPoint, selected ? System.Windows.Forms.VisualStyles.CheckBoxState.CheckedNormal : System.Windows.Forms.VisualStyles.CheckBoxState.UncheckedNormal); + var numberBounds = new Rectangle(29, bounds.Top, bounds.Width - 32, bounds.Height); + TextRenderer.DrawText(args.Graphics, (args.RowIndex + 1).ToString(), grid.Font, numberBounds, Muted, TextFormatFlags.VerticalCenter | TextFormatFlags.Left | TextFormatFlags.NoPadding); + } + + private void ToggleOriginalRow(object sender, DataGridViewCellMouseEventArgs args) + { + if (args.RowIndex < 0 || args.RowIndex >= originalGrid.Rows.Count) + return; + var row = originalGrid.Rows[args.RowIndex]; + bool wasSelected = row.Tag is bool && (bool)row.Tag; + bool isSelected = !wasSelected; + row.Tag = isSelected; + foreach (DataGridViewCell cell in row.Cells) + cell.Selected = isSelected; + originalGrid.InvalidateRow(args.RowIndex); + BeginInvoke(new Action(RefreshCheckedRowSelections)); + statusLabel.Text = isSelected + ? "Zeile " + (args.RowIndex + 1) + " vollständig ausgewählt." + : "Zeile " + (args.RowIndex + 1) + " aus der vollständigen Auswahl entfernt."; + statusLabel.ForeColor = Muted; + } + + private void RefreshCheckedRowSelections() + { + foreach (DataGridViewRow row in originalGrid.Rows) + { + bool isSelected = row.Tag is bool && (bool)row.Tag; + if (!isSelected) + continue; + foreach (DataGridViewCell cell in row.Cells) + cell.Selected = true; + } + originalGrid.Invalidate(); + } + + private void ImportCsv() + { + using (var dialog = new OpenFileDialog()) + { + dialog.Title = "CSV-Datei importieren"; + dialog.Filter = "CSV-Dateien (*.csv)|*.csv|Textdateien (*.txt)|*.txt|Alle Dateien (*.*)|*.*"; + dialog.CheckFileExists = true; + dialog.Multiselect = false; + if (dialog.ShowDialog(this) != DialogResult.OK) + return; + + try + { + using (var wizard = new ImportWizardForm(dialog.FileName)) + { + if (wizard.ShowDialog(this) != DialogResult.OK) + return; + document = wizard.SelectedDocument; + } + importedPath = dialog.FileName; + PopulateGrids(); + fileLabel.Text = Path.GetFileName(dialog.FileName) + " • " + document.Rows.Count + " Zeilen • " + document.Headers.Count + " importierte Spalten • " + DelimiterName(document.Delimiter); + statusLabel.Text = "Import erfolgreich. Wähle links Zellen aus oder nutze ‚Alle Einträge‘."; + statusLabel.ForeColor = Color.FromArgb(29, 132, 88); + SetDocumentControlsEnabled(true); + } + catch (Exception exception) + { + MessageBox.Show(this, "Die CSV-Datei konnte nicht importiert werden.\r\n\r\n" + exception.Message, "Import fehlgeschlagen", MessageBoxButtons.OK, MessageBoxIcon.Error); + statusLabel.Text = "Import fehlgeschlagen."; + statusLabel.ForeColor = Color.FromArgb(190, 60, 55); + } + } + } + + private string DelimiterName(char delimiter) + { + if (delimiter == ';') return "Semikolon"; + if (delimiter == ',') return "Komma"; + if (delimiter == '\t') return "Tabulator"; + return delimiter.ToString(); + } + + private void PopulateGrids() + { + originalGrid.SuspendLayout(); + resultGrid.SuspendLayout(); + originalGrid.Columns.Clear(); + resultGrid.Columns.Clear(); + originalGrid.Rows.Clear(); + resultGrid.Rows.Clear(); + + for (int column = 0; column < document.Headers.Count; column++) + { + string key = "Column" + column; + originalGrid.Columns.Add(key, document.Headers[column]); + resultGrid.Columns.Add(key, document.Headers[column]); + originalGrid.Columns[column].SortMode = DataGridViewColumnSortMode.NotSortable; + resultGrid.Columns[column].SortMode = DataGridViewColumnSortMode.NotSortable; + } + + foreach (var row in document.Rows) + { + originalGrid.Rows.Add(row.Cast().ToArray()); + resultGrid.Rows.Add(row.Cast().ToArray()); + } + + for (int row = 0; row < document.Rows.Count; row++) + { + originalGrid.Rows[row].HeaderCell.Value = (row + 1).ToString(); + originalGrid.Rows[row].Tag = false; + resultGrid.Rows[row].HeaderCell.Value = (row + 1).ToString(); + } + + originalGrid.ClearSelection(); + resultGrid.ClearSelection(); + resultGrid.CurrentCell = null; + originalGrid.ResumeLayout(); + resultGrid.ResumeLayout(); + } + + private void ApplyShift(int amount) + { + if (document == null) + return; + + var cells = GetTargetCells(); + if (cells.Count == 0) + { + MessageBox.Show(this, "Bitte markiere mindestens eine Zelle in der linken Tabelle oder wähle als Bereich ‚Alle Einträge‘.", "Keine Auswahl", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + int changed = 0; + foreach (var coordinate in cells) + { + var cell = resultGrid.Rows[coordinate.Item1].Cells[coordinate.Item2]; + string before = Convert.ToString(cell.Value) ?? string.Empty; + string after = LetterShifter.Shift(before, amount); + cell.Value = after; + if (!string.Equals(before, after, StringComparison.Ordinal)) + changed++; + } + + statusLabel.Text = changed + " von " + cells.Count + " Zellen verändert (" + (amount > 0 ? "+" : string.Empty) + amount + ")."; + statusLabel.ForeColor = Color.FromArgb(29, 132, 88); + } + + private List> GetTargetCells() + { + var result = new List>(); + if (scopeComboBox.SelectedIndex == 2) + { + for (int row = 0; row < resultGrid.Rows.Count; row++) + for (int column = 0; column < resultGrid.Columns.Count; column++) + result.Add(Tuple.Create(row, column)); + return result; + } + + if (scopeComboBox.SelectedIndex == 1) + { + var current = originalGrid.CurrentCell; + if (current != null) + result.Add(Tuple.Create(current.RowIndex, current.ColumnIndex)); + return result; + } + + foreach (DataGridViewCell cell in originalGrid.SelectedCells) + result.Add(Tuple.Create(cell.RowIndex, cell.ColumnIndex)); + foreach (DataGridViewRow row in originalGrid.Rows) + { + bool wholeRow = row.Tag is bool && (bool)row.Tag; + if (!wholeRow) + continue; + for (int column = 0; column < resultGrid.Columns.Count; column++) + result.Add(Tuple.Create(row.Index, column)); + } + return result.Distinct().ToList(); + } + + private void ResetResults() + { + if (document == null) + return; + for (int row = 0; row < document.Rows.Count; row++) + for (int column = 0; column < document.Headers.Count; column++) + resultGrid.Rows[row].Cells[column].Value = document.Rows[row][column]; + statusLabel.Text = "Alle Ergebnisse wurden auf die importierten Werte zurückgesetzt."; + statusLabel.ForeColor = Muted; + } + + private void ExportCsv() + { + if (document == null) + return; + + using (var dialog = new SaveFileDialog()) + { + dialog.Title = "Veränderte CSV exportieren"; + dialog.Filter = "CSV-Dateien (*.csv)|*.csv|Alle Dateien (*.*)|*.*"; + dialog.DefaultExt = "csv"; + dialog.AddExtension = true; + string sourceName = string.IsNullOrEmpty(importedPath) ? "ergebnis" : Path.GetFileNameWithoutExtension(importedPath) + "_veraendert"; + dialog.FileName = sourceName + ".csv"; + dialog.InitialDirectory = string.IsNullOrEmpty(importedPath) ? Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory) : Path.GetDirectoryName(importedPath); + if (dialog.ShowDialog(this) != DialogResult.OK) + return; + + try + { + var rows = new List>(); + foreach (DataGridViewRow gridRow in resultGrid.Rows) + { + var values = new List(); + foreach (DataGridViewCell cell in gridRow.Cells) + values.Add(Convert.ToString(cell.Value) ?? string.Empty); + rows.Add(values); + } + CsvCodec.Save(dialog.FileName, document, rows); + statusLabel.Text = "Export erfolgreich: " + dialog.FileName; + statusLabel.ForeColor = Color.FromArgb(29, 132, 88); + MessageBox.Show(this, "Die veränderte CSV wurde erfolgreich gespeichert.", "Export abgeschlossen", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + catch (Exception exception) + { + MessageBox.Show(this, "Die CSV-Datei konnte nicht gespeichert werden.\r\n\r\n" + exception.Message, "Export fehlgeschlagen", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + } + + private void SyncScroll(DataGridView source, DataGridView target) + { + if (source.Rows.Count == 0 || target.Rows.Count == 0) + return; + try + { + target.FirstDisplayedScrollingRowIndex = source.FirstDisplayedScrollingRowIndex; + target.HorizontalScrollingOffset = source.HorizontalScrollingOffset; + } + catch (ArgumentOutOfRangeException) + { + } + } + + private void SetDocumentControlsEnabled(bool enabled) + { + scopeComboBox.Enabled = enabled; + if (enabled && scopeComboBox.SelectedIndex < 0 && scopeComboBox.Items.Count > 0) + scopeComboBox.SelectedIndex = 0; + stepNumeric.Enabled = enabled; + exportButton.Enabled = enabled; + resetButton.Enabled = enabled; + scopeComboBox.Refresh(); + } + + internal void LoadPreviewData() + { + document = new CsvDocument { Delimiter = ';', FirstRowIsHeader = true }; + document.Headers.AddRange(new[] { "Kundennummer", "Vorname", "Nachname", "Ort" }); + document.Rows.Add(new List { "1001", "Anna", "Meyer", "Berlin" }); + document.Rows.Add(new List { "1002", "Jonas", "Schmidt", "Hamburg" }); + document.Rows.Add(new List { "1003", "Zoe", "Fischer", "München" }); + document.Rows.Add(new List { "1004", "Lena", "Wagner", "Köln" }); + PopulateGrids(); + for (int column = 0; column < document.Headers.Count; column++) + resultGrid.Rows[0].Cells[column].Value = LetterShifter.Shift(document.Rows[0][column], 1); + originalGrid.Rows[0].Tag = true; + RefreshCheckedRowSelections(); + fileLabel.Text = "beispiel.csv • 4 Zeilen • 4 Spalten • Trennzeichen: Semikolon"; + statusLabel.Text = "3 von 4 Zellen verändert (+1)."; + statusLabel.ForeColor = Color.FromArgb(29, 132, 88); + SetDocumentControlsEnabled(true); + scopeComboBox.SelectedIndex = 0; + scopeComboBox.Refresh(); + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..dac98d4 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# PANDA + +**P**seudonymisierung **a**lphanumerischer **N**utzdaten **d**urch **A**lphabetverschiebung + +PANDA importiert CSV-Dateien, pseudonymisiert ausgewählte Werte durch eine umkehrbare Alphabetverschiebung und exportiert das Ergebnis wieder als CSV. + +## Fest installieren + +`PANDA-Setup.exe` doppelt anklicken. Der Installer: + +- installiert PANDA für das aktuelle Windows-Benutzerkonto, +- kann Desktop- und Startmenü-Verknüpfungen anlegen, +- trägt PANDA unter **Windows → Installierte Apps** ein, +- installiert einen vollständigen Uninstaller. + +Administratorrechte sind bei der Standardinstallation nicht erforderlich. + +## Ohne Installation starten + +Alternativ `PANDA-Portable.exe` doppelt anklicken. Diese Einzeldatei benötigt keine zusätzlichen Programmdateien. + +## Bedienung + +1. **Importieren** anklicken und eine CSV-Datei auswählen. +2. Festlegen, ob die erste Zeile Überschriften enthält. +3. Spalten an- oder abwählen. Die Dateivorschau aktualisiert sich sofort. +4. Im Importfenster **Importieren** anklicken. +5. Links einzelne oder mehrere Zellen markieren. Über die Checkbox neben einer Zeilennummer lässt sich die komplette Zeile auswählen. Alternativ **Alle Einträge** wählen. +6. Schrittweite festlegen und **Hochzählen (+)** oder **Runterzählen (-)** verwenden. +7. Das Ergebnis rechts kontrollieren und mit **CSV exportieren** speichern. + +Auswahlmarkierungen werden ausschließlich in der linken Originaltabelle dargestellt. + +Die Buchstaben `A-Z` und `a-z` werden zyklisch verschoben. Beispiel: `Z + 1 = A` und `a - 1 = z`. Zahlen, Leerzeichen, Satzzeichen und Umlaute bleiben unverändert. + +## Dateien + +- `PANDA-Setup.exe` – Installer inklusive Uninstaller +- `PANDA-Portable.exe` – portable Einzeldatei +- `PANDA.ico` / `PANDA-icon-final.png` – Programmsymbol +- `Beispiel.csv` – Beispieldaten +- `Program.cs` – Programmquellcode +- `Installer.cs` / `Uninstaller.cs` – Setupquellcode +- `build.ps1` / `Tests.cs` – Build und automatische Tests diff --git a/Tests.cs b/Tests.cs new file mode 100644 index 0000000..12d615a --- /dev/null +++ b/Tests.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace Panda +{ + internal static class Tests + { + private static int failures; + + private static void Main() + { + AssertEqual("BCD YZA", LetterShifter.Shift("ABC XYZ", 1), "shift up and wrap"); + AssertEqual("Zab", LetterShifter.Shift("Abc", -1), "shift down and preserve case"); + AssertEqual("Öl 123!", LetterShifter.Shift("Öl 123!", 0), "zero shift"); + AssertEqual("Öm 123!", LetterShifter.Shift("Öl 123!", 1), "non A-Z characters unchanged"); + + string sample = "Name;Notiz\r\n\"Meyer, Anna\";\"Hallo; Welt\"\r\nBob;\"Zeile 1\r\nZeile 2\"\r\n"; + AssertEqual(';', CsvCodec.DetectDelimiter(sample), "delimiter detection"); + List> parsed = CsvCodec.Parse(sample, ';'); + AssertEqual(3, parsed.Count, "record count"); + AssertEqual("Meyer, Anna", parsed[1][0], "quoted comma"); + AssertEqual("Hallo; Welt", parsed[1][1], "quoted delimiter"); + AssertEqual("Zeile 1\r\nZeile 2", parsed[2][1], "quoted newline"); + + var source = new CsvDocument { Delimiter = ';', FirstRowIsHeader = true }; + source.Headers.AddRange(new[] { "A", "B", "C" }); + source.Rows.Add(new List { "a1", "b1", "c1" }); + source.Rows.Add(new List { "a2", "b2", "c2" }); + var filtered = CsvCodec.SelectColumns(source, new List { 2, 0 }); + AssertEqual(2, filtered.Headers.Count, "selected column count"); + AssertEqual("C", filtered.Headers[0], "selected column order"); + AssertEqual("a2", filtered.Rows[1][1], "selected column values"); + + string tempPath = Path.Combine(Path.GetTempPath(), "csv-buchstaben-test-" + Guid.NewGuid().ToString("N") + ".csv"); + try + { + var doc = new CsvDocument { Delimiter = ';', FirstRowIsHeader = true }; + doc.Headers.Add("Name"); + doc.Headers.Add("Notiz"); + var rows = new List> + { + new List { "Anna", "Hallo; \"Welt\"" }, + new List { "Bob", "Mehr\r\nZeilig" } + }; + CsvCodec.Save(tempPath, doc, rows); + var loaded = CsvCodec.Load(tempPath, true); + AssertEqual("Hallo; \"Welt\"", loaded.Rows[0][1], "save/load quotes"); + AssertEqual("Mehr\r\nZeilig", loaded.Rows[1][1], "save/load newline"); + } + finally + { + if (File.Exists(tempPath)) File.Delete(tempPath); + } + + if (failures > 0) + { + Console.Error.WriteLine(failures + " test(s) failed."); + Environment.Exit(1); + } + Console.WriteLine("All tests passed."); + } + + private static void AssertEqual(T expected, T actual, string name) + { + if (!EqualityComparer.Default.Equals(expected, actual)) + { + failures++; + Console.Error.WriteLine("FAIL " + name + ": expected [" + expected + "] actual [" + actual + "]"); + } + } + } +} diff --git a/Uninstaller.cs b/Uninstaller.cs new file mode 100644 index 0000000..4b2536a --- /dev/null +++ b/Uninstaller.cs @@ -0,0 +1,87 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Text; +using System.Windows.Forms; +using Microsoft.Win32; + +[assembly: AssemblyTitle("PANDA Uninstaller")] +[assembly: AssemblyDescription("Deinstallationsprogramm für PANDA")] +[assembly: AssemblyProduct("PANDA")] +[assembly: AssemblyCompany("PANDA")] +[assembly: AssemblyVersion("1.3.0.0")] +[assembly: AssemblyFileVersion("1.3.0.0")] + +namespace PandaUninstall +{ + internal static class Program + { + [STAThread] + private static void Main(string[] args) + { + Application.EnableVisualStyles(); + bool silent = args.Length > 0 && string.Equals(args[0], "--silent", StringComparison.OrdinalIgnoreCase); + string installDirectory = Path.GetFullPath(AppDomain.CurrentDomain.BaseDirectory); + string ownPath = Assembly.GetExecutingAssembly().Location; + + if (!silent) + { + DialogResult answer = MessageBox.Show("Soll PANDA vollständig von diesem Benutzerkonto entfernt werden?", "PANDA deinstallieren", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2); + if (answer != DialogResult.Yes) + return; + } + + try + { + DeleteIfExists(Path.Combine(installDirectory, "PANDA.exe")); + DeleteIfExists(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), "PANDA.lnk")); + + string startMenuFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Programs), "PANDA"); + DeleteIfExists(Path.Combine(startMenuFolder, "PANDA.lnk")); + DeleteIfExists(Path.Combine(startMenuFolder, "PANDA deinstallieren.lnk")); + if (Directory.Exists(startMenuFolder) && Directory.GetFileSystemEntries(startMenuFolder).Length == 0) + Directory.Delete(startMenuFolder, false); + + Registry.CurrentUser.DeleteSubKeyTree(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\PANDA", false); + + if (!silent) + MessageBox.Show("PANDA wurde erfolgreich deinstalliert.", "Deinstallation abgeschlossen", MessageBoxButtons.OK, MessageBoxIcon.Information); + + ScheduleSelfRemoval(ownPath, installDirectory); + } + catch (Exception exception) + { + if (!silent) + MessageBox.Show("PANDA konnte nicht vollständig entfernt werden.\r\n\r\n" + exception.Message + "\r\n\r\nSchließe PANDA und versuche es erneut.", "Deinstallationsfehler", MessageBoxButtons.OK, MessageBoxIcon.Error); + Environment.ExitCode = 2; + } + } + + private static void DeleteIfExists(string path) + { + if (File.Exists(path)) + File.Delete(path); + } + + private static void ScheduleSelfRemoval(string ownPath, string installDirectory) + { + string scriptPath = Path.Combine(Path.GetTempPath(), "panda-uninstall-" + Guid.NewGuid().ToString("N") + ".cmd"); + var script = new StringBuilder(); + script.AppendLine("@echo off"); + script.AppendLine("ping 127.0.0.1 -n 2 > nul"); + script.AppendLine("del /f /q \"" + ownPath + "\" > nul 2>&1"); + script.AppendLine("rmdir \"" + installDirectory.TrimEnd(Path.DirectorySeparatorChar) + "\" > nul 2>&1"); + script.AppendLine("del /f /q \"%~f0\" > nul 2>&1"); + File.WriteAllText(scriptPath, script.ToString(), Encoding.ASCII); + Process.Start(new ProcessStartInfo + { + FileName = "cmd.exe", + Arguments = "/c call \"" + scriptPath + "\"", + CreateNoWindow = true, + UseShellExecute = false, + WindowStyle = ProcessWindowStyle.Hidden + }); + } + } +} diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..73a408d --- /dev/null +++ b/build.ps1 @@ -0,0 +1,40 @@ +$ErrorActionPreference = 'Stop' +$compiler = 'C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe' +$project = Split-Path -Parent $MyInvocation.MyCommand.Path +$program = Join-Path $project 'Program.cs' +$tests = Join-Path $project 'Tests.cs' +$installerSource = Join-Path $project 'Installer.cs' +$uninstallerSource = Join-Path $project 'Uninstaller.cs' +$icon = Join-Path $project 'PANDA.ico' +$testExe = Join-Path $project 'PANDA.Tests.exe' +$appExe = Join-Path $project 'PANDA-Portable.exe' +$uninstallerExe = Join-Path $project 'PANDA-Uninstall.Payload.exe' +$setupExe = Join-Path $project 'PANDA-Setup.exe' +$iconArgument = "/win32icon:$icon" + +if (-not (Test-Path -LiteralPath $icon)) { throw 'PANDA.ico fehlt.' } + +& $compiler /nologo /target:exe /main:Panda.Tests /out:$testExe /reference:System.dll /reference:System.Core.dll /reference:System.Drawing.dll /reference:System.Windows.Forms.dll $program $tests +if ($LASTEXITCODE -ne 0) { throw 'Test-Build fehlgeschlagen.' } + +& $testExe +if ($LASTEXITCODE -ne 0) { throw 'Tests fehlgeschlagen.' } + +& $compiler /nologo /target:winexe /main:Panda.Program /optimize+ /platform:anycpu /out:$appExe $iconArgument /reference:System.dll /reference:System.Core.dll /reference:System.Drawing.dll /reference:System.Windows.Forms.dll $program +if ($LASTEXITCODE -ne 0) { throw 'Programm-Build fehlgeschlagen.' } + +& $compiler /nologo /target:winexe /main:PandaUninstall.Program /optimize+ /platform:anycpu /out:$uninstallerExe $iconArgument /reference:System.dll /reference:System.Core.dll /reference:System.Windows.Forms.dll $uninstallerSource +if ($LASTEXITCODE -ne 0) { throw 'Uninstaller-Build fehlgeschlagen.' } + +$appResource = "/resource:$appExe,PANDA.Application.exe" +$uninstallerResource = "/resource:$uninstallerExe,PANDA.Uninstaller.exe" +& $compiler /nologo /target:winexe /main:PandaSetup.Program /optimize+ /platform:anycpu /out:$setupExe $iconArgument /reference:System.dll /reference:System.Core.dll /reference:System.Drawing.dll /reference:System.Windows.Forms.dll $appResource $uninstallerResource $installerSource +if ($LASTEXITCODE -ne 0) { throw 'Installer-Build fehlgeschlagen.' } + +$verifyProcess = Start-Process -FilePath $setupExe -ArgumentList '--verify' -WindowStyle Hidden -Wait -PassThru +if ($verifyProcess.ExitCode -ne 0) { throw 'Installer-Payload-Prüfung fehlgeschlagen.' } + +Remove-Item -LiteralPath $testExe -Force +Remove-Item -LiteralPath $uninstallerExe -Force +Write-Host "Portable Einzeldatei erstellt: $appExe" +Write-Host "Installer mit Uninstaller erstellt: $setupExe"