view ARClient/Form1.cs @ 1:27ccad26a830 default tip

Fixed resource expiration
author Ivo Smits <Ivo@UCIS.nl>
date Thu, 04 Dec 2014 21:45:51 +0100
parents 90ea68d4f92f
children
line wrap: on
line source

???using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Windows.Forms;
using UCIS.NaCl.v2;
using UCIS.Util;

namespace ARClient {
	partial class Form1 : Form {
		public Boolean DatabaseSaved { get; set; }
		public String Database { get; private set; }
		public ed25519keypair SecretKey { get; set; }
		public MARCKey PublicKey { get; set; }
		public Dictionary<MARCLabel, MARCUpdate> Resources { get; private set; }
		public List<String> Peers { get; private set; }
		public List<MARCKey> BlockedKeys { get; private set; }
		public ResourceTypeFilter FilterFlags { get; set; }

		public enum ResourceTypeFilter : uint {
			AnonetIPv4 = 1,
			AnonetIPv6 = 2,
			AnonetDomain = 4,
			ASNumber= 8,
			OtherIPv4 = 16,
			OtherIPv6 = 32,
			OtherDomain = 64,
			Other = 128,
			Invalid = 256,
			Key = 512,

			None = 0,
			All = 0xffffffff,
		}

		public Form1() {
			InitializeComponent();
		}

		private void Form1_Load(object sender, EventArgs e) {
			DatabaseSaved = true;
			String dbpath = Path.Combine(Environment.CurrentDirectory, "marc.mdb");
			if (!File.Exists(dbpath) || !LoadDatabase(dbpath)) {
				MessageBox.Show("The standard database could not be loaded. The system will now generate a new key pair. If you already have a key pair and want to use it, make sure to import the key file or open the existing database.", "MARC - New database", MessageBoxButtons.OK, MessageBoxIcon.Information);
				CreateDatabase();
				if (!File.Exists(dbpath)) Database = dbpath;
			}
			cmbResourceFilter.SelectedIndex = 0;
		}

		public void UpdatePeersList() {
			synchronizeResourcesToolStripMenuItem.DropDownItems.Clear();
			ToolStripItem item = synchronizeResourcesToolStripMenuItem.DropDownItems.Add("All peers");
			item.Tag = null;
			item.Click += SynchronizeServerToolStripMenuItem_Click;
			foreach (String peer in Peers) {
				item = synchronizeResourcesToolStripMenuItem.DropDownItems.Add(peer);
				item.Tag = peer;
				item.Click += SynchronizeServerToolStripMenuItem_Click;
			}
		}

		internal Boolean IsResourceFiltered(MARCUpdate update) {
			if (BlockedKeys.Contains(update.Key)) return true;
			Byte[] label = update.Label.Bytes;
			if (label == null || label.Length < 1) return 0 != (FilterFlags & ResourceTypeFilter.Invalid);
			switch (label[0]) {
				case 0:
					if (label.Length != 33 || !ArrayUtil.Equal(ArrayUtil.Slice(label, 1), update.Key.Bytes)) return 0 == (FilterFlags & ResourceTypeFilter.Invalid);
					return 0 == (FilterFlags & ResourceTypeFilter.Key);
				case 1:
					if (label.Length != 6) return 0 == (FilterFlags & ResourceTypeFilter.Invalid);
					if (label[5] > 32) return 0 == (FilterFlags & ResourceTypeFilter.Invalid);
					if (label[1] == 1) return 0 == (FilterFlags & ResourceTypeFilter.AnonetIPv4);
					return 0 == (FilterFlags & ResourceTypeFilter.OtherIPv4);
				case 2:
					if (label.Length != 18) return 0 == (FilterFlags & ResourceTypeFilter.Invalid);
					if (label[17] > 128) return 0 == (FilterFlags & ResourceTypeFilter.Invalid);
					if (label[1] == 0xfc || label[1] == 0xfd) return 0 == (FilterFlags & ResourceTypeFilter.AnonetIPv6);
					return 0 == (FilterFlags & ResourceTypeFilter.OtherIPv6);
				case 3:
					if (label.Length != 5) return 0 == (FilterFlags & ResourceTypeFilter.Invalid);
					return 0 == (FilterFlags & ResourceTypeFilter.ASNumber);
				case 4:
					String dom = Encoding.UTF8.GetString(label, 1, label.Length - 1);
					if (dom.EndsWith(".ano")) return 0 == (FilterFlags & ResourceTypeFilter.AnonetDomain);
					return 0 == (FilterFlags & ResourceTypeFilter.OtherDomain);
				default:
					return 0 == (FilterFlags & ResourceTypeFilter.Other);
			}
		}

		public void GenerateKeyPair() {
			if (DialogResult.Yes != MessageBox.Show(this, "Generating a new key pair will erase your old key pair from the local database. If you lose your key you will no longer be able to update your resource claims. Please make sure that you either have a backup of your key, or have no claims! Do you want to continue?", "MARC - Warning", MessageBoxButtons.YesNo, MessageBoxIcon.Exclamation)) {
				return;
			}
			if (DialogResult.Yes != MessageBox.Show(this, "Are you really sure you want to overwrite your key? There is no way back! Did you make a backup?", "MARC - Warning", MessageBoxButtons.YesNo, MessageBoxIcon.Exclamation)) {
				return;
			}
			SecretKey = new ed25519keypair();
			PublicKey = new MARCKey(SecretKey.PublicKey);
			DatabaseSaved = false;
		}

		#region "Database UI stuff"
		private Boolean WarnDatabaseSaved() {
			if (DatabaseSaved) return true;
			return DialogResult.Yes == MessageBox.Show(this, "The current database is not saved. You may lose data if you continue. Losing your key pair will render your unable to update your resources. Are you sure you want to continue?", "MARC - Warning", MessageBoxButtons.YesNo, MessageBoxIcon.Exclamation);
		}
		private Boolean SaveDatabase() {
			if (Database == null) {
				return SaveDatabaseAs();
			} else {
				return SaveDatabase(Database);
			}
		}
		private Boolean SaveDatabaseAs() {
			SaveFileDialog dialog = new SaveFileDialog();
			dialog.OverwritePrompt = true;
			dialog.Title = "MARC - Save database file";
			SetupFileDialog(dialog);
			if (dialog.ShowDialog(this) != DialogResult.OK) return false;
			return SaveDatabase(dialog.FileName);
		}
		private String SelectDatabaseOpen(String title) {
			OpenFileDialog dialog = new OpenFileDialog();
			dialog.Multiselect = false;
			dialog.ShowReadOnly = false;
			dialog.CheckFileExists = true;
			dialog.Title = "MARC - " + title;
			SetupFileDialog(dialog);
			if (dialog.ShowDialog(this) != DialogResult.OK) return null;
			return dialog.FileName;
		}
		private void SetupFileDialog(FileDialog dialog) {
			dialog.AddExtension = true;
			dialog.CheckPathExists = true;
			dialog.DefaultExt = "mdb";
			dialog.DereferenceLinks = true;
			dialog.FileName = Database;
			dialog.Filter = "MARC Database files (*.mdb)|*.mdb|All files (*.*)|*.*";
			dialog.InitialDirectory = Database == null ? Environment.CurrentDirectory : Path.GetDirectoryName(Database);
			dialog.SupportMultiDottedExtensions = true;
			dialog.ValidateNames = true;
		}
		private void Form1_FormClosing(object sender, FormClosingEventArgs e) {
			if (DatabaseSaved) return;
			switch (MessageBox.Show(this, "The current database has not been saved. Losing your key pair may render you unable to update your resources! Do you want to save the database?", "MARC - Warning", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Warning)) {
				case DialogResult.Yes:
					if (!SaveDatabase()) e.Cancel = true;
					break;
				case DialogResult.No:
					break;
				case DialogResult.Cancel:
					e.Cancel = true;
					break;
			}
		}
		#endregion

		#region "Database file"
		private Boolean LoadDatabase(String fn) {
			if (!WarnDatabaseSaved()) return false;
			try {
				lstResources.BeginUpdate();
				lstResources.Sorted = false;
				using (FileStream fs = File.OpenRead(fn)) {
					BinaryReader br = new BinaryReader(fs, Encoding.UTF8);
					if (br.ReadString() != "MARC") throw new Exception("This file is not a MARC database");
					Byte version = br.ReadByte();
					Resources = new Dictionary<MARCLabel, MARCUpdate>();
					Peers = new List<string>();
					BlockedKeys = new List<MARCKey>();
					FilterFlags = ResourceTypeFilter.All;
					lstResources.Items.Clear();
					if (version == 2) {
						SecretKey = new ed25519keypair(br.ReadBytes(32));
						PublicKey = new MARCKey(SecretKey.PublicKey);

						Int32 c = br.ReadInt32();
						for (int i = 0; i < c; i++) {
							Int32 len = br.ReadInt32();
							if (len == 0) continue;
							ImportUpdate(br.ReadBytes(len), true);
						}

						c = br.ReadInt32();
						for (int i = 0; i < c; i++) Peers.Add(br.ReadString());
						c = br.ReadInt32();
						for (int i = 0; i < c; i++) BlockedKeys.Add(new MARCKey(br.ReadBytes(32)));

						FilterFlags = (ResourceTypeFilter)br.ReadInt32();
					} else if (version == 3) {
						UInt32 len = Functions.FromBigEndian((UInt32)br.ReadInt32());
						{
							Byte[] metabytes = br.ReadBytes((int)len);
							IDictionary metadata = MARCUpdate.DecodeValue(metabytes) as IDictionary;
							if (metadata != null) {
								if (metadata["keys"] != null)
									foreach (IDictionary metakey in (IList)metadata["keys"]) {
										if (!metakey.Contains("key")) continue;
										SecretKey = new ed25519keypair(((BinaryString)metakey["key"]).Bytes);
										PublicKey = new MARCKey(SecretKey.PublicKey);
										break;
									}
								if (metadata["peers"] != null)
									foreach (IDictionary metapeer in (IList)metadata["peers"])
										Peers.Add(metapeer["address"].ToString());
								if (metadata["blockedkeys"] != null)
									foreach (BinaryString metablockedkey in (IList)metadata["blockedkeys"])
										BlockedKeys.Add(new MARCKey(metablockedkey.Bytes));
								if (metadata["filterflags"] != null)
									FilterFlags = (ResourceTypeFilter)Functions.DecodeInt32BigEndian(((BinaryString)metadata["filterflags"]).Bytes, 0);
							}
						}
						while (true) {
							Byte[] lenbytes = br.ReadBytes(4);
							if (lenbytes.Length == 0) break;
							if (lenbytes.Length != 4) throw new Exception("Database truncated");
							len = Functions.DecodeInt32BigEndian(lenbytes, 0);
							ImportUpdate(br.ReadBytes((int)len), true);
						}
					} else {
						throw new Exception("Unsupported database version");
					}
					br.Close();
					Database = fn;
					DatabaseSaved = true;
					UpdatePeersList();
					return true;
				}
			} catch (Exception ex) {
				Console.Error.WriteLine(ex.ToString());
				MessageBox.Show(this, "Failed to load the database: " + ex.Message, "MARC - Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
				return false;
			} finally {
				lstResources.Sorted = true;
				lstResources.EndUpdate();
			}
		}
		private Boolean ImportDatabase(String fn) {
			try {
				lstResources.BeginUpdate();
				lstResources.Sorted = false;
				using (FileStream fs = File.OpenRead(fn)) {
					BinaryReader br = new BinaryReader(fs, Encoding.UTF8);
					if (br.ReadString() != "MARC") throw new Exception("This file is not a MARC database");
					Byte version = br.ReadByte();
					if (version == 2) {
						br.ReadBytes(32);
						Int32 c = br.ReadInt32();
						for (int i = 0; i < c; i++) {
							Int32 len = br.ReadInt32();
							if (len == 0) continue;
							ImportUpdate(br.ReadBytes(len), false);
						}
					} else if (version == 3) {
						UInt32 len = Functions.FromBigEndian((UInt32)br.ReadInt32());
						br.ReadBytes((int)len);
						while (true) {
							Byte[] lenbytes = br.ReadBytes(4);
							if (lenbytes.Length == 0) break;
							if (lenbytes.Length != 4) throw new Exception("Database truncated");
							len = Functions.DecodeInt32BigEndian(lenbytes, 0);
							ImportUpdate(br.ReadBytes((int)len), false);
						}
					} else {
						throw new Exception("Unsupported database version");
					}
					br.Close();
					return true;
				}
			} catch (Exception ex) {
				Console.Error.WriteLine(ex.ToString());
				MessageBox.Show(this, "Failed to import the database: " + ex.Message, "MARC - Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
				return false;
			} finally {
				lstResources.Sorted = true;
				lstResources.EndUpdate();
			}
		}
		private Boolean SaveDatabase(String fn) {
			try {
				if (File.Exists(fn) && fn != Database) {
					if (DialogResult.No == MessageBox.Show(this, "The file " + fn + " exists. Are you sure you want to overwrite it?", "MARC - Warning", MessageBoxButtons.YesNo, MessageBoxIcon.Warning)) {
						return false;
					}
				}
				String tempfile = Path.GetTempFileName();
				using (FileStream fs = File.Create(tempfile)) {
					BinaryWriter wr = new BinaryWriter(fs, Encoding.UTF8);
					wr.Write("MARC");
					wr.Write((Byte)3); //Version
					{
						Hashtable metadata = new Hashtable();
						ArrayList metakeys = new ArrayList();
						Hashtable metakey = new Hashtable();
						metakey.Add("key", SecretKey.SecretKey);
						metakeys.Add(metakey);
						metadata.Add("keys", metakeys);
						ArrayList metapeers = new ArrayList();
						foreach (String peer in Peers) {
							Hashtable metapeer = new Hashtable();
							metapeer.Add("address", peer);
							metapeers.Add(metapeer);
						}
						metadata.Add("peers", metapeers);
						ArrayList metablockedkeys = new ArrayList();
						foreach (MARCKey peer in BlockedKeys) metablockedkeys.Add(peer.Bytes);
						metadata.Add("blockedkeys", metablockedkeys);
						metadata.Add("filterflags", Functions.EncodeInt32BigEndian((uint)FilterFlags));
						Byte[] metabytes = MARCUpdate.EncodeValue(metadata);
						wr.Write(Functions.EncodeInt32BigEndian((uint)metabytes.Length));
						wr.Write(metabytes);
					}
					lock (Resources) {
						foreach (MARCUpdate update in Resources.Values) {
							Byte[] message = update.UpdateMessage;
							if (message == null) {
								wr.Write((Int32)0);
							} else {
								wr.Write((Int32)Functions.ToBigEndian((UInt32)message.Length));
								wr.Write(message);
							}
						}
					}
					wr.Close();
					File.Delete(fn);
					File.Move(tempfile, fn);
					Database = fn;
					DatabaseSaved = true;
					return true;
				}
			} catch (Exception ex) {
				Console.Error.WriteLine(ex.ToString());
				MessageBox.Show(this, "Failed to save the database: " + ex.Message, "MARC - Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
				return false;
			}
		}
		#endregion

		#region "Other database management"
		private void CreateDatabase() {
			if (!WarnDatabaseSaved()) return;
			Database = null;
			SecretKey = new ed25519keypair();
			PublicKey = new MARCKey(SecretKey.PublicKey);
			Resources = new Dictionary<MARCLabel, MARCUpdate>();
			Peers = new List<string>();
			BlockedKeys = new List<MARCKey>();
			FilterFlags = ResourceTypeFilter.All & ~ResourceTypeFilter.Invalid;
			Peers.Add("http://marc.ucis.ano/");
			lstResources.Items.Clear();
			UpdatePeersList();
			DatabaseSaved = true;
		}
		public Boolean ImportUpdate(Byte[] update, Boolean quick) {
			try {
				MARCUpdate resource = MARCUpdate.Decode(update);
				MARCLabel label = resource.Label;
				MARCUpdate current;
				if (!quick && IsResourceFiltered(resource)) return false;
				lock (Resources) {
					if (Resources.TryGetValue(label, out current)) {
						if (current.Serial >= resource.Serial) return false;
						if (!current.CanUpdate(resource.Key)) return false;
					} else {
						if (resource.CanDelete) return false;
						current = null;
					}
					if (!quick && !resource.VerifySignature()) return false;
					DatabaseSaved = false;
					Resources[label] = resource;
				}
				if (InvokeRequired) {
					Invoke((MethodInvoker)delegate() {
						ResourceHasChanged(resource, current);
					});
				} else {
					ResourceHasChanged(resource, current);
				}
				return true;
			} catch (Exception ex) {
				Console.Error.WriteLine(ex.ToString());
				return false;
			}
		}
		#endregion

		public void ReplaceOrAddResource(MARCUpdate newres) {
			MARCUpdate old;
			lock (Resources) {
				if (!Resources.TryGetValue(newres.Label, out old)) old = null;
				Resources[newres.Label] = newres;
				ResourceHasChanged(newres, old);
			}
		}
		public void ResourceHasChanged(MARCUpdate newres, MARCUpdate oldres) {
			Boolean selected = false;
			if (oldres != null) {
				selected = lstResources.SelectedItem == oldres;
				lstResources.Items.Remove(oldres);
			}
			if (newres != null) {
				AddResourceToListFiltered(newres);
				if (selected) {
					lstResources.SelectedItem = newres;
					ShowResourceDetails(newres);
				}
			}
		}

		#region "Resource list box"
		private void cmbResourceFilter_SelectedIndexChanged(object sender, EventArgs e) {
			RefreshResourceList();
		}
		public void RefreshResourceList() {
			lstResources.BeginUpdate();
			lstResources.Sorted = false;
			lstResources.Items.Clear();
			lock (Resources) foreach (MARCUpdate update in Resources.Values) AddResourceToListFiltered(update);
			lstResources.Sorted = true;
			lstResources.EndUpdate();
		}
		private void AddResourceToListFiltered(MARCUpdate update) {
			switch (cmbResourceFilter.SelectedIndex) {
				case 1: //My resources
					if (update.Key != PublicKey) return;
					break;
				case 2: //Filtered items
					if (!IsResourceFiltered(update)) return;
					break;
				case 3: //Expired items
					if (!update.Expired) return;
					break;
				case 4: //Domain names
					if (update.Label.Bytes.Length < 1 || update.Label.Bytes[0] != 4) return;
					break;
				case 5: //IPv4 networks
					if (update.Label.Bytes.Length != 6 || update.Label.Bytes[0] != 1) return;
					break;
				case 6: //IPv6 networks
					if (update.Label.Bytes.Length != 18 || update.Label.Bytes[0] != 2) return;
					break;
				case 7: //AS numbers
					if (update.Label.Bytes.Length != 5 || update.Label.Bytes[0] != 3) return;
					break;
				case 8: //Key info
					if (update.Label.Bytes.Length != 33 || update.Label.Bytes[0] != 0) return;
					break;
				case 0: //All resources
				default:
					break;
			}
			lstResources.Items.Add(update);
		}
		private void lstResources_SelectedIndexChanged(object sender, EventArgs e) {
			if (lstResources.SelectedItem == null) return;
			ShowResourceDetails((MARCUpdate)lstResources.SelectedItem);
		}
		#endregion

		#region "Resource details area"
		private void ShowResourceDetails(MARCUpdate update) {
			txtResourceKey.Text = update.Key.ToString();
			txtResourceLabel.Text = update.Label.ToString();
			txtResourceSerial.Text = update.Serial.ToString();
			UInt32 tsnow = checked((UInt32)(DateTime.UtcNow - (new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc))).TotalSeconds);
			UInt32 tslastyear = tsnow - 365 * 24 * 60 * 60;
			String[] flags = null;
			if (PublicKey == update.Key) UCIS.Util.ArrayUtil.Add(ref flags, "Owned");
			else if (update.CanUpdate(PublicKey)) UCIS.Util.ArrayUtil.Add(ref flags, "Transferable");
			if (update.Expired) UCIS.Util.ArrayUtil.Add(ref flags, "Expired");
			if (flags == null || flags.Length == 0) UCIS.Util.ArrayUtil.Add(ref flags, "Claimed");
			txtResourceStatus.Text = String.Join(", ", flags);
			lvwResourceValue.SuspendLayout();
			lvwResourceValue.Nodes.Clear();

			if (update.Value is IDictionary) {
				foreach (DictionaryEntry child in (IDictionary)update.Value) lvwResourceValue.Nodes.Add(AddResourceValueInTree(child.Key.ToString() + ": ", child.Value));
			} else if (update.Value is ICollection) {
				foreach (Object child in (ICollection)update.Value) lvwResourceValue.Nodes.Add(AddResourceValueInTree(String.Empty, child));
			} else if (update.Value == null) {
				lvwResourceValue.Nodes.Add("Null");
			} else {
				lvwResourceValue.Nodes.Add(update.Value.ToString());
			}

			lvwResourceValue.ResumeLayout();
		}
		private TreeNode AddResourceValueInTree(String prefix, Object value) {
			TreeNode node = new TreeNode();
			if (value is IDictionary) {
				node.Text = prefix + "Dictionary";
				foreach (DictionaryEntry child in (IDictionary)value) node.Nodes.Add(AddResourceValueInTree(child.Key.ToString() + ": ", child.Value));
			} else if (value is ICollection) {
				node.Text = prefix + "Collection";
				foreach (Object child in (ICollection)value) node.Nodes.Add(AddResourceValueInTree(String.Empty, child));
			} else if (value == null) {
				node.Text = prefix + "Null";
			} else {
				node.Text = prefix + value.ToString();
			}
			node.Expand();
			return node;
		}
		#endregion

		#region "Menu bar"
		private void settingsToolStripMenuItem_Click(object sender, EventArgs e) {
			frmSettings form = new frmSettings();
			form.MainForm = this;
			form.ShowDialog(this);
		}

		public void newDatabaseToolStripMenuItem_Click(object sender, EventArgs e) {
			CreateDatabase();
		}
		public void reloadDatabaseToolStripMenuItem_Click(object sender, EventArgs e) {
			if (Database == null || !File.Exists(Database)) {
				openDatabaseToolStripMenuItem_Click(sender, e);
			} else {
				LoadDatabase(Database);
			}
		}
		public void saveDatabaseToolStripMenuItem_Click(object sender, EventArgs e) {
			SaveDatabase();
		}
		public void saveDatabaseAsToolStripMenuItem_Click(object sender, EventArgs e) {
			SaveDatabaseAs();
		}
		public void openDatabaseToolStripMenuItem_Click(object sender, EventArgs e) {
			String fn = SelectDatabaseOpen("Open database");
			if (fn == null) return;
			LoadDatabase(fn);
		}
		public void importDatabaseToolStripMenuItem_Click(object sender, EventArgs e) {
			String fn = SelectDatabaseOpen("Open database");
			if (fn == null) return;
			ImportDatabase(fn);
		}
		private void SynchronizeServerToolStripMenuItem_Click(object sender, EventArgs e) {
			ToolStripMenuItem tsi = sender as ToolStripMenuItem;
			frmSync form = new frmSync();
			form.MainForm = this;
			form.Hosts = (tsi == null || tsi.Tag == null) ? (IEnumerable<String>)Peers : new String[] { tsi.Tag.ToString() };
			form.ShowDialog(this);
		}

		private void deleteSelectedToolStripMenuItem_Click(object sender, EventArgs e) {
			MessageBox.Show(this, "This will delete the resources only from the local database. It may be retrieved again during synchronization.", "MARC - Delete resource", MessageBoxButtons.OK, MessageBoxIcon.Information);
			List<MARCUpdate> ToDelete = new List<MARCUpdate>();
			foreach (Object item in lstResources.SelectedItems) {
				MARCUpdate update = (MARCUpdate)item;
				if (Resources.Remove(update.Label)) ToDelete.Add(update);
				DatabaseSaved = false;
			}
			lstResources.BeginUpdate();
			lstResources.Sorted = false;
			foreach (MARCUpdate item in ToDelete) ResourceHasChanged(null, item);
			lstResources.Sorted = true;
			lstResources.EndUpdate();
		}

		private void addKeyToFilterListToolStripMenuItem_Click(object sender, EventArgs e) {
			if (lstResources.SelectedItems.Count == 0) return;
			foreach (Object item in lstResources.SelectedItems) {
				MARCUpdate update = (MARCUpdate)item;
				if (BlockedKeys.Contains(update.Key)) continue;
				BlockedKeys.Add(update.Key);
				DatabaseSaved = false;
			}
			MessageBox.Show(this, "The selected key has been added to the filter list. Use the Settings window to delete all filtered items from the local database.", "MARC - Key filter", MessageBoxButtons.OK, MessageBoxIcon.Information);
		}

		private void addNewToolStripMenuItem_Click(object sender, EventArgs e) {
			frmEditResource form = new frmEditResource();
			form.MainForm = this;
			form.ShowDialog(this);
		}

		private void editSelectedToolStripMenuItem_Click(object sender, EventArgs e) {
			MARCUpdate resource = lstResources.SelectedItem as MARCUpdate;
			if (resource == null) return;
			frmEditResource form = new frmEditResource();
			form.MainForm = this;
			form.Label = resource.Label.Bytes;
			form.Value = resource.Value as IDictionary;
			form.ImportData();
			form.ShowDialog(this);
		}
		#endregion

		private void lstResources_DoubleClick(object sender, EventArgs e) {
			editSelectedToolStripMenuItem_Click(sender, e);
		}
	}
}