diff options
author | lloyd <[email protected]> | 2006-06-03 06:04:12 +0000 |
---|---|---|
committer | lloyd <[email protected]> | 2006-06-03 06:04:12 +0000 |
commit | 7f60cc296a2c58febcaa8f6b5f20fa4f6cba8460 (patch) | |
tree | 73c6dac3e940073e28c160636b7999979910a654 | |
parent | 42996f8d0aeb2c7f039852cff229edb94c45967b (diff) |
Add the GTK+ example code to misc/
-rw-r--r-- | misc/gtk/GNUmakefile | 24 | ||||
-rw-r--r-- | misc/gtk/dsa.cpp | 552 | ||||
-rw-r--r-- | misc/gtk/gtk_ui.cpp | 78 | ||||
-rw-r--r-- | misc/gtk/gtk_ui.h | 25 | ||||
-rw-r--r-- | misc/gtk/readme.txt | 22 |
5 files changed, 701 insertions, 0 deletions
diff --git a/misc/gtk/GNUmakefile b/misc/gtk/GNUmakefile new file mode 100644 index 000000000..d0e81505b --- /dev/null +++ b/misc/gtk/GNUmakefile @@ -0,0 +1,24 @@ + +CC = g++ + +LIBS=$(shell botan-config --libs) $(shell pkg-config --libs gtk+-2.0) +IFLAGS=$(shell botan-config --cflags) $(shell pkg-config --cflags gtk+-2.0) +FLAGS=-Wall -W $(IFLAGS) + +SOURCE=$(wildcard *.cpp) +HEADERS=$(wildcard *.h) + +OBJS=$(patsubst %.cpp,%.o,$(SOURCE)) + +APP=dsa + +all: $(APP) + +$(APP): $(OBJS) + $(CC) $(OBJS) $(LIBS) -o $(APP) + +%.o: %.cpp $(HEADERS) + $(CC) $(FLAGS) -c $< + +clean: + rm -f $(APP) $(OBJS) diff --git a/misc/gtk/dsa.cpp b/misc/gtk/dsa.cpp new file mode 100644 index 000000000..a5471e9c0 --- /dev/null +++ b/misc/gtk/dsa.cpp @@ -0,0 +1,552 @@ +/* + This shows some of the details involved in a GUI application that uses + Botan. Actually most of the code is just dealing with GTK+, but it shows how + the password callback and pulse function stuff works. (See gtk_ui.cpp for the + acutal password callback code.) + + The major points of interest (assuming what you care about is how to use + Botan from a GUI, and not looking at my terrible GTK code) are gtk_ui.cpp + and, in this file, gtk_pulse(), gen_key(), and get_key(): + + gtk_ui.cpp and get_key() show how to get a passphrase from a user for + decrypting (well, in theory, anything), but in this case, PKCS #8 private + keys. Which is coincidentally the only thing Botan currently uses UI + objects for, though that will probably change eventually. GTK_UI does + double duty, for getting passphrases for encryption as well (in + do_save_key). + + gen_key() and gtk_pulse() show how to do an activity meter while doing a + long-term operation inside Botan. Since, typically, the only operations + which take a long time and can't be broken up into smaller parts are prime + generation/testing, that is currently where the pulse hooks are + called. It's certainly not the most general callback method in the world, + but it's general enough that it's usable without getting in the way too + much. The callbacks will eventually be extended to other parts of the + library (Pipe, maybe) where it's useful. + + This program is in the public domain. +*/ +#include <fstream> +#include <iostream> +#include <memory> + +#include <botan/botan.h> +#include <botan/look_pk.h> +#include <botan/filters.h> +#include <botan/dsa.h> +// we don't have a 'using namespace' here, so it's easy to grep for code that +// is actually dealing with the library (rather than messing around with GTK). + +#if BOTAN_VERSION_CODE < BOTAN_VERSION_CODE_FOR(1,4,4) + #error "You need a more recent version of Botan (at least 1.4.4)" +#endif + +#include <gtk/gtk.h> +#include "gtk_ui.h" + +/************************************************* +* Pop up an message box * +*************************************************/ +static void show_dialog(const std::string& about_message, + const std::string& dialog_name) + { + GtkDialogFlags flags = + (GtkDialogFlags)(GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL); + + GtkWidget* dialog = gtk_dialog_new_with_buttons(dialog_name.c_str(), + NULL, flags, + GTK_STOCK_OK, + GTK_RESPONSE_NONE, + NULL); + GtkWidget* label = gtk_label_new(NULL); + gtk_label_set_markup(GTK_LABEL(label), about_message.c_str()); + gtk_label_set_justify(GTK_LABEL(label), GTK_JUSTIFY_CENTER); + + g_signal_connect_swapped(GTK_OBJECT(dialog), "response", + G_CALLBACK(gtk_widget_destroy), + GTK_OBJECT(dialog)); + + gtk_container_add(GTK_CONTAINER(GTK_DIALOG(dialog)->vbox), label); + gtk_widget_show_all(dialog); + } + +/************************************************* +* Pop up an About box * +*************************************************/ +static void show_about() + { + const std::string about_message = + "<big>DSA Utility</big>\n" + "\n" + "A simple application showing how to use Botan within a GUI.\n" + "It lets you generate or load keys, and sign text files.\n" + "\n" + "Send comments/questions to <tt>[email protected]</tt>"; + + show_dialog(about_message, "About"); + } + +/************************************************* +* Pop up a help box * +*************************************************/ +static void show_help() + { + const std::string help_message = + "<big>DSA Utility Help</big>\n" + "\n" + "Simply, this is a (very) simple text editor, with the added ability\n" + "of being able to generate or read a DSA private key, and sign the\n" + "text buffer using that key.\n" + "\n" + "You can load, edit, and save text files as you would normally. If a\n" + "key is loaded (done using the commands in the Keys menu), you can\n" + "also use the Sign command (in the Signing menu) to generate a\n" + "signature for the current file. It will be displayed at the bottom\n" + "of the screen (if it has been calculated for the current buffer\n" + "contents), and can be saved using the \"Save Sig\" command.\n" + "\n" + "Signatures generated by this program can be verified using a the\n" + "<tt>dsa_ver</tt> example included in the Botan distribution.\n"; + + show_dialog(help_message, "Help"); + } + +/************************************************* +* Get and return a filename from the user * +*************************************************/ +static std::string get_filename(const std::string& title) + { + GtkWidget* dialog = gtk_file_selection_new(title.c_str()); + + /* Some window managers don't display the titles of transient windows, + put a message elsewhere for those people. + */ + GtkWidget* label = gtk_label_new(title.c_str()); + gtk_container_add(GTK_CONTAINER(GTK_DIALOG(dialog)->vbox), label); + + std::string fsname; + + gtk_widget_show(label); /* dialog_run won't show sub-widgets */ + if(gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_OK) + fsname = gtk_file_selection_get_filename(GTK_FILE_SELECTION(dialog)); + gtk_widget_destroy(dialog); + + /* If it's a directory, that's no good */ + if(fsname.size() && fsname[fsname.size()-1] == '/') + return ""; + + return fsname; + } + +/************************************************* +* Global state * +*************************************************/ +static Botan::DSA_PrivateKey* key = 0; // our key +static GtkTextBuffer* buffer = 0; // the text buffer +static std::string buffer_source; + // what file (if any) the buffer's data came from +static GtkWidget* sig_view = 0; // the signature + +/************************************************* +* Zap the currently set signature (if any) * +*************************************************/ +static void zap_sig() + { + gtk_editable_delete_text(GTK_EDITABLE(sig_view), 0, -1); + } + +/************************************************* +* Save the current key * +*************************************************/ +static void do_save_key(const std::string& title) + { + if(key == 0) + return; + + std::string filename = get_filename(title.c_str()); + + if(filename != "") + { + const std::string msg = "Select a passphrase to encrypt the key:"; + + std::ofstream out_priv(filename.c_str()); + + GTK_UI ui; + Botan::User_Interface::UI_Result result; + std::string passphrase = ui.get_passphrase(msg, result); + + if(result == Botan::User_Interface::OK) + out_priv << Botan::PKCS8::PEM_encode(*key, passphrase); + else + out_priv << Botan::PKCS8::PEM_encode(*key); + + // for testing + //std::cout << X509::PEM_encode(*key); + } + } + +/************************************************* +* Generate a signature for the text buffer * +*************************************************/ +static void sign_buffer() + { + /* No key? Ignore request. */ + if(key == 0) + return; + + /* same format as the text-mode dsa_sign example */ + Botan::Pipe pipe(new Botan::PK_Signer_Filter( + Botan::get_pk_signer(*key, "EMSA1(SHA-1)") + ), + new Botan::Base64_Encoder + ); + + /* It would probably be smart to do this a little bit at a time */ + GtkTextIter start, end; + gtk_text_buffer_get_bounds(buffer, &start, &end); + gchar* bits = gtk_text_buffer_get_text(buffer, &start, &end, FALSE); + size_t length = strlen(bits); + + pipe.start_msg(); + pipe.write((unsigned char*)bits, length); + pipe.end_msg(); + + std::string sig = pipe.read_all_as_string(); + + zap_sig(); + + gint position = 0; + gtk_editable_insert_text(GTK_EDITABLE(sig_view), sig.c_str(), sig.length(), + &position); + + g_free(bits); + } + +/************************************************* +* GTK+ pulse callback * +*************************************************/ +static void gtk_pulse(Botan::UI::Pulse_Type, void*) + { + /* We need this to flush the udpates, otherwise GTK+ will wait until we're + done with the computation before doing any updates (generally the right + thing, but not with a progress bar). + */ + while(gtk_events_pending()) + gtk_main_iteration(); + } + +/************************************************* +* Actual do the pulse (as a GTK+ timeout func) * +*************************************************/ +static gboolean gtk_pulse_timeout(void* pbar) + { + GtkWidget* progress_bar = (GtkWidget*)pbar; + gtk_progress_bar_pulse(GTK_PROGRESS_BAR(progress_bar)); + return TRUE; /* keep calling us */ + } + +/************************************************* +* Generate a new key * +*************************************************/ +static void gen_key() + { + /* This gives a nice smooth progress bar, though we do use up quite a bit of + CPU for it. Keep in mind that if PULSE_INTERVAL is significantly less + than the average time between pulses from the library, the progress bar + will jerk around going slower or faster. Keep it at at least 50ms. + */ + const double PROGRESS_PER_PULSE = .01; /* % of bar */ + const guint32 PULSE_INTERVAL = 30; /* ms */ + + delete key; + + GtkDialogFlags flags = + (GtkDialogFlags)(GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL); + + GtkWidget* dialog = + gtk_dialog_new_with_buttons("Generating Key", NULL, flags, NULL); + + GtkWidget* label = gtk_label_new(" Generating new key, please wait... \n"); + gtk_container_add(GTK_CONTAINER(GTK_DIALOG(dialog)->vbox), label); + + GtkWidget* progress_bar = gtk_progress_bar_new(); + gtk_progress_bar_set_pulse_step(GTK_PROGRESS_BAR(progress_bar), + PROGRESS_PER_PULSE); + gtk_container_add(GTK_CONTAINER(GTK_DIALOG(dialog)->vbox), progress_bar); + + guint timer_id = gtk_timeout_add(PULSE_INTERVAL, gtk_pulse_timeout, + progress_bar); + + gtk_widget_show_all(dialog); + + while(gtk_events_pending()) + gtk_main_iteration(); + + /* Register gtk_pulse so it will be called every so often when we embark + on our prime generation quest... + */ + /* this just updates the GUI; the GTK+ timeout function actually updates + the progress bar. That's because the amount of time between pulses + from the library is rather irregular, so the progress bar looks jerky. + */ + Botan::UI::set_pulse(gtk_pulse); + + /* Not generally recommended, since it's slow and there's not much point. + However, *because* it's slow, we'll want to put up a progress bar or + something, and part of this whole thing is to show how to do that and get + the pulse functions to do the right thing. + */ + Botan::DL_Group group(1024, Botan::DL_Group::DSA_Kosherizer); + key = new Botan::DSA_PrivateKey(group); + + gtk_timeout_remove(timer_id); + Botan::UI::set_pulse(0); // unset the pulse function + + gtk_widget_destroy(dialog); + + do_save_key("Save New Key"); + + /* new key, any old sigs are no longer useful */ + zap_sig(); + } + +/************************************************* +* Load up a key * +*************************************************/ +static void get_key() + { + std::string fsname = get_filename("Select a DSA Key"); + + if(fsname != "") + { + try { + delete key; + key = 0; + zap_sig(); + + /* + A GTK_UI is a subclass of User_Interface that pops up a dialog that + asks the user for a passphrase. It actually works quite well, + though the fixed upper limit on the passphrase size is not + ideal. Feel free to use it as-is or modify it however you like + (gtk_ui.* is public domain). + */ + GTK_UI ui; + Botan::PKCS8_PrivateKey* p8_key = Botan::PKCS8::load_key(fsname, ui); + key = dynamic_cast<Botan::DSA_PrivateKey*>(p8_key); + if(!key) + show_dialog("The key in " + fsname + " is not a DSA key", + "Failure"); + } + catch(std::exception) + { + key = 0; // make sure it's not something random + show_dialog("Loading the key from " + fsname + " failed.", "Failure"); + } + } + } + +static void really_sign_buffer() + { + /* No key? Ask the user for one. */ + if(key == 0) + get_key(); + sign_buffer(); + } + +/************************************************* +* Clear the text buffer * +*************************************************/ +static void new_buffer() + { + /* + In theory, it would be nice to check if this was unsaved text and prompt + to save it. However, this really isn't supposed to be a GTK+ example, so + we won't. + */ + gtk_text_buffer_set_text(buffer, "", -1); + buffer_source = ""; + } + +/************************************************* +* Put the contents of a file into the buffer * +*************************************************/ +static void open_buffer() + { + std::string filename = get_filename("Select File"); + + if(filename == "") + return; + + std::ifstream in(filename.c_str()); + + new_buffer(); + buffer_source = filename; + + while(in.good()) + { + char buf[1024] = { 0 }; + + in.read(buf, 1024); + size_t got = in.gcount(); + + GtkTextIter iter; + gtk_text_buffer_get_end_iter(buffer, &iter); + gtk_text_buffer_insert(buffer, &iter, buf, got); + } + } + +/************************************************* +* Save the signature to a file * +*************************************************/ +static void save_sig() + { + std::string sig_file = buffer_source; + + /* No sig, nothing to save */ + const gchar* sig = gtk_entry_get_text(GTK_ENTRY(sig_view)); + if(strlen(sig) == 0) + return; + + if(sig_file == "") + sig_file = get_filename("Select Signature Output File"); + else + sig_file += ".sig"; + + std::ofstream out(sig_file.c_str()); + out << sig << std::endl; + } + +/************************************************* +* Save the current key * +*************************************************/ +static void save_key() + { + do_save_key("Save Current Key"); + } + +/************************************************* +* Common case of Save/Save As * +*************************************************/ +static void do_save(const std::string& filename) + { + std::ofstream out(filename.c_str()); + + GtkTextIter start, end; + gtk_text_buffer_get_bounds(buffer, &start, &end); + gchar* bits = gtk_text_buffer_get_text(buffer, &start, &end, FALSE); + out.write(bits, strlen(bits)); + g_free(bits); + buffer_source = filename; + } + +/************************************************* +* Save the buffer * +*************************************************/ +static void save_buffer_as() + { + std::string filename = get_filename("Select Output File"); + if(filename != "") + do_save(filename); + } + +/************************************************* +* Save the buffer * +*************************************************/ +static void save_buffer() + { + if(buffer_source != "") + do_save(buffer_source); + else + save_buffer_as(); + } + +/************************************************* +* Make a menubar for the app * +*************************************************/ +static GtkWidget* make_menubar(GtkWidget *window) + { + static GtkItemFactoryEntry menu_items[] = { + { "/_File", NULL, NULL, 0, "<Branch>", NULL }, + { "/File/_New", "<control>N", new_buffer, 0, NULL, NULL }, + { "/File/_Open", "<control>O", open_buffer, 0, NULL, NULL }, + { "/File/_Save", "<control>S", save_buffer, 0, NULL, NULL }, + { "/File/Save _As", NULL, save_buffer_as, 0, NULL, NULL }, + { "/File/sep1", NULL, NULL, 0, "<Separator>", NULL }, + { "/File/Save Sig", NULL, save_sig, 0, NULL, NULL }, + { "/File/sep2", NULL, NULL, 0, "<Separator>", NULL }, + { "/File/_Quit", "<control>Q", gtk_main_quit, 0, NULL, NULL }, + + { "/_Keys", NULL, NULL, 0, "<Branch>", NULL }, + { "/Keys/Open", NULL, get_key, 0, NULL, NULL }, + { "/Keys/_Generate", NULL, gen_key, 0, NULL, NULL }, + { "/Keys/Save Current", NULL, save_key, 0, NULL, NULL }, + + { "/Signing", NULL, NULL, 0, "<Branch>", NULL }, + { "/Signing/Sign", NULL, really_sign_buffer, 0, NULL, NULL }, + + { "/_Help", NULL, NULL, 0, "<LastBranch>", NULL }, + { "/Help/Help", NULL, show_help, 0, NULL, NULL }, + { "/Help/sep1", NULL, NULL, 0, "<Separator>", NULL }, + { "/Help/About", NULL, show_about, 0, NULL, NULL }, + }; + + GtkAccelGroup* accel_group = gtk_accel_group_new(); + GtkItemFactory* item_factory = + gtk_item_factory_new(GTK_TYPE_MENU_BAR, "<main>", accel_group); + const gint nmenu_items = sizeof(menu_items) / sizeof(menu_items[0]); + gtk_item_factory_create_items(item_factory, nmenu_items, menu_items, NULL); + gtk_window_add_accel_group(GTK_WINDOW(window), accel_group); + return gtk_item_factory_get_widget(item_factory, "<main>"); + } + +int main(int argc, char *argv[]) + { + gtk_init(&argc, &argv); + + try { + Botan::LibraryInitializer init; + + /* Create a new top-level window */ + GtkWidget* window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + gtk_window_set_title(GTK_WINDOW(window), "DSA Utility"); + gtk_signal_connect(GTK_OBJECT(window), "delete_event", + gtk_main_quit, NULL); + + /* Create the vbox to hold our stuff */ + GtkWidget* vbox = gtk_vbox_new(FALSE, 0); + gtk_container_border_width(GTK_CONTAINER(vbox), 1); + gtk_container_add(GTK_CONTAINER(window), vbox); + + /* Create the menu bar */ + GtkWidget *menubar = make_menubar(window); + + /* Create the entry that holds the signature */ + sig_view = gtk_entry_new(); + gtk_editable_set_editable(GTK_EDITABLE(sig_view), FALSE); + + /* Create the text box */ + GtkWidget* view = gtk_text_view_new(); + buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(view)); + + gtk_widget_set_size_request(view, 640, 480); + gtk_text_buffer_set_text(buffer, "Some initial contents.", -1); + + // Resign it on each change: fast enough, but probably not really useful + //g_signal_connect(G_OBJECT(buffer), "changed", sign_buffer, 0); + g_signal_connect(G_OBJECT(buffer), "changed", zap_sig, 0); + + gtk_container_add(GTK_CONTAINER(vbox), menubar); + gtk_container_add(GTK_CONTAINER(vbox), view); + gtk_container_add(GTK_CONTAINER(vbox), sig_view); + + gtk_widget_show_all(window); + + gtk_main(); + } + catch(std::exception& e) + { + std::cout << e.what() << std::endl; + } + return 0; + } diff --git a/misc/gtk/gtk_ui.cpp b/misc/gtk/gtk_ui.cpp new file mode 100644 index 000000000..515fbc519 --- /dev/null +++ b/misc/gtk/gtk_ui.cpp @@ -0,0 +1,78 @@ +/************************************************* +* GTK+ User Interface Source File * +*************************************************/ + +#include "gtk_ui.h" + +/************************************************* +* GTK+ Callback * +*************************************************/ +void GTK_UI::callback(GtkWidget* entry, gpointer passphrase_ptr) + { + const gchar *entry_text = gtk_entry_get_text(GTK_ENTRY(entry)); + char* passphrase = (char*)passphrase_ptr; + strcpy(passphrase, entry_text); + } + +/************************************************* +* Get a passphrase from the user * +*************************************************/ +std::string GTK_UI::get_passphrase(const std::string& what, + const std::string& source, + UI_Result& result) const + { + std::string msg = "A passphrase is needed to access the " + what; + if(source != "") msg += "\nin " + source; + return get_passphrase(msg, result); + } + +/************************************************* +* Get a passphrase from the user * +*************************************************/ +std::string GTK_UI::get_passphrase(const std::string& label_text, + UI_Result& result) const + { + const int MAX_PASSPHRASE = 64; + + GtkDialogFlags flags = + (GtkDialogFlags)(GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL); + + GtkWidget* dialog = gtk_dialog_new_with_buttons( + "Enter Passphrase", + NULL, flags, + GTK_STOCK_OK, GTK_RESPONSE_OK, + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + NULL); + + gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_OK); + + GtkWidget* label = gtk_label_new(label_text.c_str()); + + GtkWidget* entry = gtk_entry_new(); + gtk_entry_set_visibility(GTK_ENTRY(entry), 0); + gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE); + gtk_entry_set_max_length(GTK_ENTRY(entry), MAX_PASSPHRASE); + + char passphrase_buf[MAX_PASSPHRASE + 1] = { 0 }; + + gtk_signal_connect(GTK_OBJECT(entry), "activate", + GTK_SIGNAL_FUNC(callback), passphrase_buf); + + GtkWidget* vbox = gtk_vbox_new(FALSE, 0); + gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(vbox), entry, FALSE, FALSE, 0); + + gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog)->vbox), vbox, TRUE, TRUE, 0); + gtk_widget_show_all(vbox); + + /* Block until we get something back */ + result = CANCEL_ACTION; + if(gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_OK) + result = OK; + + gtk_widget_destroy(dialog); + + if(result == OK) + return std::string(passphrase_buf); + return ""; + } diff --git a/misc/gtk/gtk_ui.h b/misc/gtk/gtk_ui.h new file mode 100644 index 000000000..9ced83a81 --- /dev/null +++ b/misc/gtk/gtk_ui.h @@ -0,0 +1,25 @@ +/************************************************* +* GTK+ User Interface Header File * +*************************************************/ + +#ifndef BOTAN_EXT_GTK_UI__ +#define BOTAN_EXT_GTK_UI__ + +#include <botan/ui.h> +#include <gtk/gtk.h> + +/************************************************* +* GTK+ Passphrase Callback Object * +*************************************************/ +class GTK_UI : public Botan::User_Interface + { + public: + std::string get_passphrase(const std::string&, const std::string&, + UI_Result&) const; + + std::string get_passphrase(const std::string&, UI_Result&) const; + + static void callback(GtkWidget*, gpointer); + }; + +#endif diff --git a/misc/gtk/readme.txt b/misc/gtk/readme.txt new file mode 100644 index 000000000..1f20732b6 --- /dev/null +++ b/misc/gtk/readme.txt @@ -0,0 +1,22 @@ + +This is an example of how to use Botan in a GUI. You need at least Botan 1.3.8, +and I would recommend 1.4.2. + +You'll also need GTK+ 2.x (I'm using 2.2, but AFAIK not any 2.2-specific +functionality). Keep in mind that I was learning GTK as I was writing this, so +it's not exactly the best GTK code you're likely to see. + +dsa.cpp is the main GTK+ driver. It has some comments at the top which point +out major areas of interest. + +gtk_ui.* implement a User_Interface object that opens up a GTK+ dialog box that +asks the user for their passphrase. It works pretty well, the only major +deficiency is a fixed upper limit on the size of the passphrase (currently 64). +You may want to use this in your own code, assuming you use GTK. If not, it +should at least provide an outline for writing a version for your favorite +windowing system. + +To build, you'll need to have GNU make, or be willing to compile it by hand. +Just go get gmake, enough real stuff needs it anyway. + +Jack |