2002-11-03 17:26:12 +01:00
|
|
|
/*
|
2005-01-12 05:24:53 +01:00
|
|
|
* The contents of this file are subject to the Initial
|
|
|
|
* Developer's Public License Version 1.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the
|
|
|
|
* License. You may obtain a copy of the License at
|
|
|
|
* http://www.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl.
|
2002-11-03 17:26:12 +01:00
|
|
|
*
|
2005-01-12 05:24:53 +01:00
|
|
|
* Software distributed under the License is distributed AS IS,
|
|
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing rights
|
|
|
|
* and limitations under the License.
|
2002-11-03 17:26:12 +01:00
|
|
|
*
|
2005-01-12 05:24:53 +01:00
|
|
|
* The Original Code was created by Dmitry Yemanov
|
|
|
|
* for the Firebird Open Source RDBMS project.
|
|
|
|
*
|
|
|
|
* Copyright (c) 2002 Dmitry Yemanov <dimitr@users.sf.net>
|
|
|
|
* and all contributors signed below.
|
|
|
|
*
|
|
|
|
* All Rights Reserved.
|
|
|
|
* Contributor(s): ______________________________________.
|
2002-11-03 17:26:12 +01:00
|
|
|
*/
|
|
|
|
|
|
|
|
#include "firebird.h"
|
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
#include "../common/classes/alloc.h"
|
|
|
|
#include "../common/classes/auto.h"
|
|
|
|
#include "../common/config/config_file.h"
|
|
|
|
#include "../common/config/config.h"
|
|
|
|
#include "../jrd/os/path_utils.h"
|
2004-04-29 00:00:03 +02:00
|
|
|
#include <stdio.h>
|
2002-11-03 17:26:12 +01:00
|
|
|
|
|
|
|
#ifdef HAVE_STDLIB_H
|
|
|
|
#include <stdlib.h>
|
|
|
|
#endif
|
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
using namespace Firebird;
|
2002-11-03 17:26:12 +01:00
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
namespace {
|
2002-11-03 17:26:12 +01:00
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
class MainStream : public ConfigFile::Stream
|
2002-11-03 17:26:12 +01:00
|
|
|
{
|
2010-02-28 19:00:51 +01:00
|
|
|
public:
|
|
|
|
MainStream(const char* fname, bool fExceptionOnError)
|
|
|
|
: file(fopen(fname, "rt")), l(0)
|
|
|
|
{
|
2010-03-01 03:14:36 +01:00
|
|
|
if (!file && fExceptionOnError)
|
2010-02-28 19:00:51 +01:00
|
|
|
{
|
|
|
|
// config file does not exist
|
2010-03-01 10:03:00 +01:00
|
|
|
fatal_exception::raiseFmt("Missing configuration file: %s", fname);
|
2010-02-28 19:00:51 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool getLine(ConfigFile::String& input, unsigned int& line)
|
2002-11-03 17:26:12 +01:00
|
|
|
{
|
2010-02-28 19:00:51 +01:00
|
|
|
input = "";
|
|
|
|
if (!file)
|
2008-05-21 15:53:17 +02:00
|
|
|
{
|
2010-02-28 19:00:51 +01:00
|
|
|
return false;
|
2008-05-21 15:53:17 +02:00
|
|
|
}
|
2010-02-28 19:00:51 +01:00
|
|
|
|
|
|
|
// this loop efficiently skips almost all comment lines
|
|
|
|
do
|
|
|
|
{
|
|
|
|
if (feof(file))
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
input.LoadFromFile(file);
|
|
|
|
++l;
|
|
|
|
input.alltrim(" \t\r");
|
|
|
|
} while (input.isEmpty() || input[0] == '#');
|
2010-03-01 03:14:36 +01:00
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
line = l;
|
2008-05-21 15:53:17 +02:00
|
|
|
return true;
|
2002-11-03 17:26:12 +01:00
|
|
|
}
|
2008-05-21 15:53:17 +02:00
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
private:
|
|
|
|
AutoPtr<FILE, FileClose> file;
|
|
|
|
unsigned int l;
|
|
|
|
};
|
2008-05-22 23:45:22 +02:00
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
class TextStream : public ConfigFile::Stream
|
|
|
|
{
|
|
|
|
public:
|
|
|
|
TextStream(const char* configText)
|
|
|
|
: s(configText), l(0)
|
2008-05-21 15:53:17 +02:00
|
|
|
{
|
2010-02-28 19:00:51 +01:00
|
|
|
if (s && !*s)
|
2008-05-21 15:53:17 +02:00
|
|
|
{
|
2010-02-28 19:00:51 +01:00
|
|
|
s = NULL;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool getLine(ConfigFile::String& input, unsigned int& line)
|
|
|
|
{
|
|
|
|
do
|
|
|
|
{
|
|
|
|
if (!s)
|
2008-05-21 15:53:17 +02:00
|
|
|
{
|
2010-02-28 19:00:51 +01:00
|
|
|
input = "";
|
2008-05-21 15:53:17 +02:00
|
|
|
return false;
|
|
|
|
}
|
2010-03-01 03:14:36 +01:00
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
const char* ptr = strchr(s, '\n');
|
|
|
|
if (!ptr)
|
2008-05-21 15:53:17 +02:00
|
|
|
{
|
2010-02-28 19:00:51 +01:00
|
|
|
input.assign(s);
|
|
|
|
s = NULL;
|
2008-05-21 15:53:17 +02:00
|
|
|
}
|
2010-02-28 19:00:51 +01:00
|
|
|
else
|
|
|
|
{
|
|
|
|
input.assign(s, ptr - s);
|
|
|
|
s = ptr + 1;
|
|
|
|
if (!*s)
|
|
|
|
{
|
|
|
|
s = NULL;
|
|
|
|
}
|
|
|
|
}
|
2010-03-01 03:14:36 +01:00
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
++l;
|
|
|
|
input.alltrim(" \t\r");
|
|
|
|
} while (input.isEmpty() || input[0] == '#');
|
2010-03-01 03:14:36 +01:00
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
line = l;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
const char* s;
|
|
|
|
unsigned int l;
|
|
|
|
};
|
|
|
|
|
|
|
|
class SubStream : public ConfigFile::Stream
|
|
|
|
{
|
|
|
|
public:
|
|
|
|
SubStream()
|
|
|
|
: cnt(0)
|
|
|
|
{ }
|
|
|
|
|
|
|
|
bool getLine(ConfigFile::String& input, unsigned int& line)
|
|
|
|
{
|
|
|
|
if (cnt >= data.getCount())
|
|
|
|
{
|
|
|
|
input = "";
|
|
|
|
return false;
|
2008-05-21 15:53:17 +02:00
|
|
|
}
|
2008-05-22 23:45:22 +02:00
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
input = data[cnt].first;
|
|
|
|
line = data[cnt].second;
|
|
|
|
++cnt;
|
2010-03-01 03:14:36 +01:00
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
return true;
|
2008-05-21 15:53:17 +02:00
|
|
|
}
|
2008-05-22 23:45:22 +02:00
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
void putLine(const ConfigFile::String& input, unsigned int line)
|
|
|
|
{
|
|
|
|
data.push(Line(input, line));
|
|
|
|
}
|
2002-11-03 17:26:12 +01:00
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
private:
|
|
|
|
typedef Pair<Left<ConfigFile::String, unsigned int> > Line;
|
|
|
|
ObjectsArray<Line> data;
|
|
|
|
size_t cnt;
|
|
|
|
};
|
2002-11-03 17:26:12 +01:00
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
} // anonymous namespace
|
2002-11-03 17:26:12 +01:00
|
|
|
|
|
|
|
|
2010-03-04 13:52:01 +01:00
|
|
|
ConfigFile::ConfigFile(const Firebird::PathName& file, USHORT fl)
|
2010-02-28 19:00:51 +01:00
|
|
|
: AutoStorage(), configFile(getPool(), file), parameters(getPool()), flags(fl)
|
|
|
|
{
|
|
|
|
MainStream s(configFile.c_str(), flags & EXCEPTION_ON_ERROR);
|
|
|
|
parse(&s);
|
2002-11-03 17:26:12 +01:00
|
|
|
}
|
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
ConfigFile::ConfigFile(const char* file, USHORT fl)
|
|
|
|
: AutoStorage(), configFile(getPool(), String(file)), parameters(getPool()), flags(fl)
|
|
|
|
{
|
|
|
|
MainStream s(configFile.c_str(), flags & EXCEPTION_ON_ERROR);
|
|
|
|
parse(&s);
|
|
|
|
}
|
2002-11-03 17:26:12 +01:00
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
ConfigFile::ConfigFile(UseText, const char* configText, USHORT fl)
|
|
|
|
: AutoStorage(), configFile(getPool()), parameters(getPool()), flags(fl)
|
2002-11-03 17:26:12 +01:00
|
|
|
{
|
2010-02-28 19:00:51 +01:00
|
|
|
TextStream s(configText);
|
|
|
|
parse(&s);
|
|
|
|
}
|
2002-11-03 17:26:12 +01:00
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
ConfigFile::ConfigFile(MemoryPool& p, ConfigFile::Stream* s, USHORT fl, const String& file)
|
|
|
|
: AutoStorage(p), configFile(getPool(), file), parameters(getPool()), flags(fl)
|
|
|
|
{
|
|
|
|
parse(s);
|
2002-11-03 17:26:12 +01:00
|
|
|
}
|
|
|
|
|
2010-03-01 03:14:36 +01:00
|
|
|
ConfigFile::Stream::~Stream()
|
|
|
|
{
|
|
|
|
}
|
2010-02-28 19:00:51 +01:00
|
|
|
|
2002-11-03 17:26:12 +01:00
|
|
|
/******************************************************************************
|
|
|
|
*
|
2010-02-28 19:00:51 +01:00
|
|
|
* Parse line, taking quotes into an account
|
2002-11-03 17:26:12 +01:00
|
|
|
*/
|
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
ConfigFile::LineType ConfigFile::parseLine(const String& input, String& key, String& value)
|
2002-11-03 17:26:12 +01:00
|
|
|
{
|
2010-02-28 19:00:51 +01:00
|
|
|
int inString = 0;
|
|
|
|
String::size_type valStart = 0;
|
|
|
|
String::size_type eol = String::npos;
|
|
|
|
bool hasSub = false;
|
|
|
|
|
|
|
|
for (String::size_type n=0; n < input.length(); ++n)
|
2008-05-21 15:53:17 +02:00
|
|
|
{
|
2010-02-28 19:00:51 +01:00
|
|
|
switch (input[n])
|
|
|
|
{
|
|
|
|
case '"':
|
|
|
|
if (key.isEmpty()) // quoted string to the left of = doesn't make sense
|
|
|
|
return LINE_BAD;
|
|
|
|
if (inString >= 2) // one more quote after quoted string doesn't make sense
|
|
|
|
return LINE_BAD;
|
|
|
|
inString++;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case '=':
|
|
|
|
key = input.substr(0, n);
|
|
|
|
key.rtrim(" \t\r");
|
|
|
|
if (key.isEmpty()) // not good - no key
|
|
|
|
return LINE_BAD;
|
|
|
|
valStart = n + 1;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case '#':
|
|
|
|
if (inString != 1)
|
|
|
|
{
|
|
|
|
eol = n;
|
|
|
|
n = input.length(); // skip the rest of symbols
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case ' ':
|
|
|
|
case '\t':
|
|
|
|
case '\r':
|
|
|
|
break;
|
|
|
|
|
|
|
|
case '{':
|
|
|
|
case '}':
|
|
|
|
if (flags & HAS_SUB_CONF)
|
|
|
|
{
|
2010-03-01 03:14:36 +01:00
|
|
|
if (inString != 1)
|
|
|
|
{
|
2010-02-28 19:00:51 +01:00
|
|
|
if (input[n] == '}') // Subconf close mark not expected
|
|
|
|
{
|
|
|
|
return LINE_BAD;
|
|
|
|
}
|
|
|
|
|
|
|
|
hasSub = true;
|
|
|
|
inString = 2;
|
|
|
|
eol = n;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
// fall through ....
|
|
|
|
|
|
|
|
default:
|
|
|
|
if (inString >= 2) // Something after the end of line
|
|
|
|
return LINE_BAD;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (inString == 1) // If we are still inside a string, it's error
|
|
|
|
return LINE_BAD;
|
2010-03-01 03:14:36 +01:00
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
if (key.isEmpty())
|
|
|
|
{
|
|
|
|
key = input.substr(0, eol);
|
|
|
|
key.rtrim(" \t\r");
|
2010-03-02 15:25:54 +01:00
|
|
|
value.erase();
|
2010-02-28 19:00:51 +01:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
value = input.substr(valStart, eol - valStart);
|
|
|
|
value.alltrim(" \t\r");
|
|
|
|
value.alltrim("\"");
|
|
|
|
}
|
|
|
|
|
|
|
|
// Now expand macros in value
|
|
|
|
String::size_type subFrom;
|
|
|
|
while ((subFrom = value.find("$(")) != String::npos)
|
|
|
|
{
|
|
|
|
String::size_type subTo = value.find(")", subFrom);
|
|
|
|
if (subTo != String::npos)
|
|
|
|
{
|
|
|
|
String macro;
|
|
|
|
String m = value.substr(subFrom + 2, subTo - (subFrom + 2));
|
|
|
|
if (! translate(m, macro))
|
|
|
|
{
|
|
|
|
return LINE_BAD;
|
|
|
|
}
|
|
|
|
value.replace(subFrom, subTo + 1 - subFrom, macro);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
return LINE_BAD;
|
|
|
|
}
|
2008-05-21 15:53:17 +02:00
|
|
|
}
|
2002-11-03 17:26:12 +01:00
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
return hasSub ? LINE_START_SUB : LINE_REGULAR;
|
2002-11-03 17:26:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/******************************************************************************
|
|
|
|
*
|
2010-02-28 19:00:51 +01:00
|
|
|
* Find macro value
|
2002-11-03 17:26:12 +01:00
|
|
|
*/
|
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
bool ConfigFile::translate(const String& from, String& to)
|
2002-11-03 17:26:12 +01:00
|
|
|
{
|
2010-02-28 19:00:51 +01:00
|
|
|
if (from == "root")
|
2008-05-21 15:53:17 +02:00
|
|
|
{
|
2010-02-28 19:00:51 +01:00
|
|
|
to = Config::getRootDirectory();
|
2008-05-21 15:53:17 +02:00
|
|
|
}
|
2010-02-28 19:00:51 +01:00
|
|
|
else if (from == "install")
|
|
|
|
{
|
|
|
|
to = Config::getInstallDirectory();
|
|
|
|
}
|
|
|
|
else if (from == "this")
|
|
|
|
{
|
|
|
|
if (configFile.isEmpty())
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
2010-03-04 13:52:01 +01:00
|
|
|
PathName path, file;
|
|
|
|
PathUtils::splitLastComponent(path, file, configFile.ToPathName());
|
|
|
|
to = path.ToString();
|
2010-02-28 19:00:51 +01:00
|
|
|
}
|
|
|
|
/* else if (!substituteOneOfStandardFirebirdDirs(from, to))
|
2008-05-21 15:53:17 +02:00
|
|
|
{
|
2010-02-28 19:00:51 +01:00
|
|
|
return false;
|
|
|
|
} */
|
|
|
|
else
|
|
|
|
{
|
|
|
|
return false;
|
2008-05-21 15:53:17 +02:00
|
|
|
}
|
2008-12-05 02:20:14 +01:00
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
return true;
|
2002-11-03 17:26:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/******************************************************************************
|
|
|
|
*
|
2010-02-28 19:00:51 +01:00
|
|
|
* Return parameter corresponding the given key
|
2002-11-03 17:26:12 +01:00
|
|
|
*/
|
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
const ConfigFile::Parameter* ConfigFile::findParameter(const String& name) const
|
2002-11-03 17:26:12 +01:00
|
|
|
{
|
2010-02-28 19:00:51 +01:00
|
|
|
size_t pos;
|
|
|
|
return parameters.find(name, pos) ? ¶meters[pos] : NULL;
|
2002-11-03 17:26:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/******************************************************************************
|
|
|
|
*
|
2010-02-28 19:00:51 +01:00
|
|
|
* Return parameter corresponding the given key and value
|
2002-11-03 17:26:12 +01:00
|
|
|
*/
|
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
const ConfigFile::Parameter* ConfigFile::findParameter(const String& name, const String& value) const
|
2002-11-03 17:26:12 +01:00
|
|
|
{
|
2010-02-28 19:00:51 +01:00
|
|
|
size_t pos;
|
|
|
|
if (!parameters.find(name, pos))
|
|
|
|
{
|
|
|
|
return NULL;
|
|
|
|
}
|
2008-05-21 15:53:17 +02:00
|
|
|
|
2010-03-01 03:14:36 +01:00
|
|
|
while (pos < parameters.getCount() && parameters[pos].name == name)
|
2008-05-21 15:53:17 +02:00
|
|
|
{
|
2010-02-28 19:00:51 +01:00
|
|
|
if (parameters[pos].value == value)
|
2004-04-11 16:47:04 +02:00
|
|
|
{
|
2010-02-28 19:00:51 +01:00
|
|
|
return ¶meters[pos];
|
2004-04-11 16:47:04 +02:00
|
|
|
}
|
2010-02-28 19:00:51 +01:00
|
|
|
++pos;
|
2008-05-21 15:53:17 +02:00
|
|
|
}
|
2010-03-01 03:14:36 +01:00
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
/******************************************************************************
|
|
|
|
*
|
|
|
|
* Take into an account fault line
|
|
|
|
*/
|
2002-11-03 17:26:12 +01:00
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
void ConfigFile::badLine(const String& line)
|
|
|
|
{
|
|
|
|
if (flags & EXCEPTION_ON_ERROR)
|
2008-05-21 15:53:17 +02:00
|
|
|
{
|
2010-03-01 03:14:36 +01:00
|
|
|
fatal_exception::raiseFmt("%s: illegal line <%s>",
|
|
|
|
(configFile.hasData() ? configFile.c_str() : "Passed text"),
|
2010-02-28 19:00:51 +01:00
|
|
|
line.c_str());
|
|
|
|
}
|
|
|
|
}
|
2002-11-03 17:26:12 +01:00
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
/******************************************************************************
|
|
|
|
*
|
|
|
|
* Load file immediately
|
|
|
|
*/
|
2008-05-21 15:53:17 +02:00
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
void ConfigFile::parse(Stream* stream)
|
|
|
|
{
|
|
|
|
String inputLine;
|
|
|
|
Parameter* previous = NULL;
|
|
|
|
unsigned int line;
|
2002-11-03 17:26:12 +01:00
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
while (stream->getLine(inputLine, line))
|
|
|
|
{
|
|
|
|
Parameter current;
|
|
|
|
current.line = line;
|
|
|
|
|
2010-03-01 03:14:36 +01:00
|
|
|
switch (parseLine(inputLine, current.name, current.value))
|
2008-05-21 15:53:17 +02:00
|
|
|
{
|
2010-02-28 19:00:51 +01:00
|
|
|
case LINE_BAD:
|
|
|
|
badLine(inputLine);
|
|
|
|
break;
|
2002-11-03 17:26:12 +01:00
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
case LINE_REGULAR:
|
|
|
|
if (current.name.isEmpty())
|
|
|
|
{
|
|
|
|
badLine(inputLine);
|
|
|
|
break;
|
|
|
|
}
|
2002-11-03 17:26:12 +01:00
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
previous = ¶meters[parameters.add(current)];
|
|
|
|
break;
|
2002-11-03 17:26:12 +01:00
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
case LINE_START_SUB:
|
|
|
|
if (current.name.hasData())
|
|
|
|
{
|
|
|
|
size_t n = parameters.add(current);
|
|
|
|
previous = ¶meters[n];
|
|
|
|
}
|
2010-03-01 03:14:36 +01:00
|
|
|
|
2010-02-28 19:00:51 +01:00
|
|
|
{ // subconf scope
|
|
|
|
SubStream subStream;
|
|
|
|
while (stream->getLine(inputLine, line))
|
|
|
|
{
|
|
|
|
if (inputLine[0] == '}')
|
|
|
|
{
|
|
|
|
String s = inputLine.substr(1);
|
|
|
|
s.ltrim(" \t\r");
|
|
|
|
if (s.hasData() && s[0] != '#')
|
|
|
|
{
|
|
|
|
badLine(s);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
subStream.putLine(inputLine, line);
|
|
|
|
}
|
|
|
|
|
2010-03-01 03:14:36 +01:00
|
|
|
previous->sub = FB_NEW(getPool()) ConfigFile(getPool(), &subStream,
|
2010-02-28 19:00:51 +01:00
|
|
|
flags & ~HAS_SUB_CONF, configFile);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
2003-04-19 18:46:24 +02:00
|
|
|
}
|
2002-11-03 17:26:12 +01:00
|
|
|
}
|