You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
697 lines
17 KiB
697 lines
17 KiB
/***************************************************************************
|
|
kgpgfile.cpp
|
|
-------------------
|
|
begin : Fri Jan 23 2004
|
|
copyright : (C) 2004,2005 by Thomas Baumgart
|
|
email : thb@net-bembel.de
|
|
***************************************************************************/
|
|
|
|
/***************************************************************************
|
|
* *
|
|
* This program is free software; you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation; either version 2 of the License, or *
|
|
* (at your option) any later version. *
|
|
* *
|
|
***************************************************************************/
|
|
|
|
#ifdef HAVE_CONFIG
|
|
#include <config.h>
|
|
#endif
|
|
|
|
#include "kdecompat.h"
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// QT Includes
|
|
|
|
#include <tqfile.h>
|
|
#include <tqdir.h>
|
|
#include <tqstring.h>
|
|
|
|
#include <tqeventloop.h>
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// KDE Includes
|
|
|
|
#include <kapplication.h>
|
|
#include <klocale.h>
|
|
#include <kprocess.h>
|
|
#include <kpassdlg.h>
|
|
#include <klibloader.h>
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Project Includes
|
|
|
|
#include "kgpgfile.h"
|
|
|
|
#if 0
|
|
class KGPGFileFactory : public KLibFactory
|
|
{
|
|
public:
|
|
KGPGFileFactory() : KLibFactory() {}
|
|
~KGPGFileFactory(){}
|
|
TQObject *createObject( TQObject *, const char *, const char*, const TQStringList & )
|
|
{
|
|
return new KGPGFile;
|
|
}
|
|
};
|
|
|
|
extern "C" {
|
|
void *init_libkgpgfile()
|
|
{
|
|
return new KGPGFileFactory;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
KGPGFile::KGPGFile(const TQString& fn, const TQString& homedir, const TQString& options) :
|
|
m_options(options),
|
|
m_homedir(homedir),
|
|
m_readRemain(0),
|
|
m_needExitLoop(false)
|
|
{
|
|
setName(fn);
|
|
m_exitStatus = -2;
|
|
m_comment = "created by KGPGFile";
|
|
// tqDebug("ungetchbuffer %d", m_ungetchBuffer.length());
|
|
}
|
|
|
|
KGPGFile::~KGPGFile()
|
|
{
|
|
close();
|
|
}
|
|
|
|
void KGPGFile::init(void)
|
|
{
|
|
setFlags(IO_Sequential);
|
|
setStatus(IO_Ok);
|
|
setState(0);
|
|
}
|
|
|
|
void KGPGFile::setName(const TQString& fn)
|
|
{
|
|
m_fn = fn;
|
|
if(fn[0] == '~') {
|
|
m_fn = TQDir::homeDirPath()+fn.mid(1);
|
|
|
|
} else if(TQDir::isRelativePath(m_fn)) {
|
|
TQDir dir(fn);
|
|
m_fn = dir.absPath();
|
|
}
|
|
// tqDebug("setName: '%s'", m_fn.data());
|
|
}
|
|
|
|
void KGPGFile::flush(void)
|
|
{
|
|
// no functionality
|
|
}
|
|
|
|
void KGPGFile::addRecipient(const TQCString& recipient)
|
|
{
|
|
m_recipient << recipient;
|
|
}
|
|
|
|
bool KGPGFile::open(int mode)
|
|
{
|
|
return open(mode, TQString(), false);
|
|
}
|
|
|
|
bool KGPGFile::open(int mode, const TQString& cmdArgs, bool skipPasswd)
|
|
{
|
|
bool useOwnPassphrase = (getenv("GPG_AGENT_INFO") == 0);
|
|
|
|
// tqDebug("KGPGFile::open(%d)", mode);
|
|
m_errmsg.resize(1);
|
|
if(isOpen()) {
|
|
// tqDebug("File already open");
|
|
return false;
|
|
}
|
|
|
|
// tqDebug("check filename empty");
|
|
if(m_fn.isEmpty())
|
|
return false;
|
|
|
|
// tqDebug("setup file structures");
|
|
init();
|
|
setMode(mode);
|
|
|
|
// tqDebug("check valid access mode");
|
|
if(!(isReadable() || isWritable()))
|
|
return false;
|
|
|
|
if(isWritable()) {
|
|
// tqDebug("check recipient count");
|
|
if(m_recipient.count() == 0)
|
|
return false;
|
|
// tqDebug("check access rights");
|
|
if(!checkAccess(m_fn, W_OK))
|
|
return false;
|
|
}
|
|
|
|
TQStringList args;
|
|
if(cmdArgs.isEmpty()) {
|
|
args << "--homedir" << TQString("\"%1\"").arg(m_homedir)
|
|
<< "-q"
|
|
<< "--batch";
|
|
|
|
if(isWritable()) {
|
|
args << "-ea"
|
|
<< "-z" << "6"
|
|
<< "--comment" << TQString("\"%1\"").arg(m_comment)
|
|
<< "--trust-model=always"
|
|
<< "-o" << TQString("\"%1\"").arg(m_fn);
|
|
TQValueList<TQCString>::Iterator it;
|
|
for(it = m_recipient.begin(); it != m_recipient.end(); ++it)
|
|
args << "-r" << TQString("\"%1\"").arg(TQString(*it));
|
|
|
|
// some versions of GPG had trouble to replace a file
|
|
// so we delete it first
|
|
TQFile::remove(m_fn);
|
|
} else {
|
|
args << "-da";
|
|
if(useOwnPassphrase)
|
|
args << "--passphrase-fd" << "0";
|
|
else
|
|
args << "--use-agent";
|
|
args << "--no-default-recipient" << TQString("\"%1\"").arg(m_fn);
|
|
}
|
|
} else {
|
|
args = TQStringList::split(" ", cmdArgs);
|
|
}
|
|
|
|
TQCString pwd;
|
|
if(isReadable() && useOwnPassphrase && !skipPasswd) {
|
|
KPasswordDialog dlg(KPasswordDialog::Password,false,0);
|
|
dlg.setPrompt(i18n("Enter passphrase"));
|
|
dlg.addLine(i18n("File"), m_fn);
|
|
dlg.adjustSize();
|
|
if (dlg.exec() == TQDialog::Rejected)
|
|
return false;
|
|
pwd = TQCString(dlg.password());
|
|
}
|
|
|
|
// tqDebug("starting GPG process");
|
|
if(!startProcess(args))
|
|
return false;
|
|
|
|
// tqDebug("check GPG process running");
|
|
if(!m_process) {
|
|
// if the process is not present anymore, we have to check
|
|
// if it was a read operation and we might already have data
|
|
// and the process finished normally. In that case, we
|
|
// just continue.
|
|
if(isReadable()) {
|
|
if(m_ungetchBuffer.isEmpty())
|
|
return false;
|
|
} else
|
|
return false;
|
|
}
|
|
|
|
if(isReadable() && useOwnPassphrase && !skipPasswd) {
|
|
// tqDebug("Passphrase is '%s'", pwd.data());
|
|
if(_writeBlock(pwd.data(), pwd.length()) == -1) {
|
|
// tqDebug("Sending passphrase failed");
|
|
return false;
|
|
}
|
|
m_process->closeStdin();
|
|
}
|
|
|
|
setState( IO_Open );
|
|
at( 0 );
|
|
// tqDebug("File open");
|
|
return true;
|
|
}
|
|
|
|
bool KGPGFile::startProcess(const TQStringList& args)
|
|
{
|
|
// now start the KProcess with GPG
|
|
m_process = new KShellProcess();
|
|
*m_process << "gpg";
|
|
*m_process << args;
|
|
|
|
// TQString arglist = args.join(":");
|
|
// tqDebug("gpg '%s'", arglist.data());
|
|
|
|
connect(m_process, TQT_SIGNAL(processExited(KProcess *)),
|
|
this, TQT_SLOT(slotGPGExited(KProcess *)));
|
|
|
|
connect(m_process, TQT_SIGNAL(receivedStdout(KProcess*, char*, int)),
|
|
this, TQT_SLOT(slotDataFromGPG(KProcess*, char*, int)));
|
|
|
|
connect(m_process, TQT_SIGNAL(receivedStderr(KProcess*, char*, int)),
|
|
this, TQT_SLOT(slotErrorFromGPG(KProcess*, char*, int)));
|
|
|
|
connect(m_process, TQT_SIGNAL(wroteStdin(KProcess *)),
|
|
this, TQT_SLOT(slotSendDataToGPG(KProcess *)));
|
|
|
|
if(!m_process->start(KProcess::NotifyOnExit, (KProcess::Communication)(KProcess::Stdin|KProcess::Stdout|KProcess::Stderr))) {
|
|
// tqDebug("m_process->start failed");
|
|
delete m_process;
|
|
m_process = 0;
|
|
return false;
|
|
}
|
|
|
|
// let the process settle and see if it starts and survives ;-)
|
|
kapp->processEvents(100);
|
|
return true;
|
|
}
|
|
|
|
void KGPGFile::close(void)
|
|
{
|
|
// tqDebug("KGPGFile::close()");
|
|
if(!isOpen()) {
|
|
// tqDebug("File not open");
|
|
return;
|
|
}
|
|
|
|
// finish the KProcess and clean up things
|
|
if(m_process) {
|
|
if(isWritable()) {
|
|
// tqDebug("Finish writing");
|
|
if(m_process->isRunning()) {
|
|
m_process->closeStdin();
|
|
// now wait for GPG to finish
|
|
m_needExitLoop = true;
|
|
tqApp->enter_loop();
|
|
} else
|
|
m_process->kill();
|
|
|
|
} else if(isReadable()) {
|
|
// tqDebug("Finish reading");
|
|
if(m_process->isRunning()) {
|
|
m_process->closeStdout();
|
|
// now wait for GPG to finish
|
|
m_needExitLoop = true;
|
|
tqApp->enter_loop();
|
|
} else
|
|
m_process->kill();
|
|
}
|
|
}
|
|
m_ungetchBuffer = TQCString();
|
|
setState(0);
|
|
m_recipient.clear();
|
|
// tqDebug("File closed");
|
|
}
|
|
|
|
int KGPGFile::getch(void)
|
|
{
|
|
if(!isOpen())
|
|
return EOF;
|
|
if(!isReadable())
|
|
return EOF;
|
|
|
|
int ch;
|
|
|
|
if(!m_ungetchBuffer.isEmpty()) {
|
|
ch = (m_ungetchBuffer)[0] & 0xff;
|
|
m_ungetchBuffer.remove(0, 1);
|
|
|
|
} else {
|
|
char buf[1];
|
|
ch = (readBlock(buf,1) == 1) ? (buf[0] & 0xff) : EOF;
|
|
}
|
|
|
|
// tqDebug("getch returns 0x%02X", ch);
|
|
return ch;
|
|
}
|
|
|
|
int KGPGFile::ungetch(int ch)
|
|
{
|
|
if(!isOpen())
|
|
return EOF;
|
|
if(!isReadable())
|
|
return EOF;
|
|
|
|
if(ch != EOF) {
|
|
// tqDebug("store 0x%02X in ungetchbuffer", ch & 0xff);
|
|
m_ungetchBuffer.insert(0, ch & 0xff);
|
|
}
|
|
|
|
return ch;
|
|
}
|
|
|
|
int KGPGFile::putch(int c)
|
|
{
|
|
char buf[1];
|
|
buf[0] = c;
|
|
if(writeBlock(buf, 1) != EOF)
|
|
return c;
|
|
return EOF;
|
|
}
|
|
|
|
TQ_LONG KGPGFile::writeBlock(const char *data, TQ_ULONG maxlen)
|
|
{
|
|
if(!isOpen())
|
|
return EOF;
|
|
if(!isWritable())
|
|
return EOF;
|
|
|
|
return _writeBlock(data, maxlen);
|
|
}
|
|
|
|
TQ_LONG KGPGFile::_writeBlock(const char *data, TQ_ULONG maxlen)
|
|
{
|
|
if(!m_process)
|
|
return EOF;
|
|
if(!m_process->isRunning())
|
|
return EOF;
|
|
|
|
if(m_process->writeStdin(data, maxlen)) {
|
|
// wait until the data has been written
|
|
m_needExitLoop = true;
|
|
tqApp->enter_loop();
|
|
if(!m_process)
|
|
return EOF;
|
|
return maxlen;
|
|
|
|
} else
|
|
return EOF;
|
|
}
|
|
|
|
TQ_LONG KGPGFile::readBlock(char *data, TQ_ULONG maxlen)
|
|
{
|
|
// char *oridata = data;
|
|
if(maxlen == 0)
|
|
return 0;
|
|
|
|
if(!isOpen())
|
|
return EOF;
|
|
if(!isReadable())
|
|
return EOF;
|
|
|
|
TQ_ULONG nread = 0;
|
|
if(!m_ungetchBuffer.isEmpty()) {
|
|
unsigned l = m_ungetchBuffer.length();
|
|
if(maxlen < l)
|
|
l = maxlen;
|
|
memcpy(data, m_ungetchBuffer, l);
|
|
nread += l;
|
|
data = &data[l];
|
|
m_ungetchBuffer.remove(0, l);
|
|
|
|
if(!m_process) {
|
|
// tqDebug("read %d bytes from unget buffer", nread);
|
|
// dumpBuffer(oridata, nread);
|
|
return nread;
|
|
}
|
|
}
|
|
|
|
// check for EOF
|
|
if(!m_process) {
|
|
// tqDebug("EOF (no process)");
|
|
return EOF;
|
|
}
|
|
|
|
m_readRemain = maxlen - nread;
|
|
m_ptrRemain = data;
|
|
if(m_readRemain) {
|
|
m_process->resume();
|
|
m_needExitLoop = true;
|
|
tqApp->enter_loop();
|
|
}
|
|
// if nothing has been read (maxlen-m_readRemain == 0) then we assume EOF
|
|
if((maxlen - m_readRemain) == 0) {
|
|
// tqDebug("EOF (nothing read)");
|
|
return EOF;
|
|
}
|
|
// tqDebug("return %d bytes", maxlen - m_readRemain);
|
|
// dumpBuffer(oridata, maxlen - m_readRemain);
|
|
return maxlen - m_readRemain;
|
|
}
|
|
|
|
TQByteArray KGPGFile::readAll(void)
|
|
{
|
|
// use a larger blocksize than in the TQIODevice version
|
|
const int blocksize = 8192;
|
|
int nread = 0;
|
|
TQByteArray ba;
|
|
while ( !atEnd() ) {
|
|
ba.resize( nread + blocksize );
|
|
int r = readBlock( ba.data()+nread, blocksize );
|
|
if ( r < 0 )
|
|
return TQByteArray();
|
|
nread += r;
|
|
}
|
|
ba.resize( nread );
|
|
return ba;
|
|
}
|
|
|
|
void KGPGFile::slotGPGExited(KProcess* )
|
|
{
|
|
// tqDebug("GPG finished");
|
|
if(m_process) {
|
|
if(m_process->normalExit()) {
|
|
m_exitStatus = m_process->exitStatus();
|
|
if(m_exitStatus != 0)
|
|
setStatus(IO_UnspecifiedError);
|
|
} else {
|
|
m_exitStatus = -1;
|
|
}
|
|
delete m_process;
|
|
m_process = 0;
|
|
}
|
|
|
|
if(m_needExitLoop) {
|
|
m_needExitLoop = false;
|
|
tqApp->exit_loop();
|
|
}
|
|
}
|
|
|
|
void KGPGFile::slotDataFromGPG(KProcess* proc, char* buf, int len)
|
|
{
|
|
// tqDebug("Received %d bytes on stdout", len);
|
|
|
|
// copy current buffer to application
|
|
int copylen;
|
|
copylen = m_readRemain < len ? m_readRemain : len;
|
|
if(copylen != 0) {
|
|
memcpy(m_ptrRemain, buf, copylen);
|
|
m_ptrRemain += copylen;
|
|
buf += copylen;
|
|
m_readRemain -= copylen;
|
|
len -= copylen;
|
|
}
|
|
|
|
// store rest of buffer in ungetch buffer
|
|
while(len--) {
|
|
m_ungetchBuffer += *buf++;
|
|
}
|
|
|
|
// if we have all the data the app requested, we can safely suspend
|
|
if(m_readRemain == 0) {
|
|
proc->suspend();
|
|
// wake up the recipient
|
|
if(m_needExitLoop) {
|
|
m_needExitLoop = false;
|
|
tqApp->exit_loop();
|
|
}
|
|
}
|
|
// tqDebug("end slotDataFromGPG");
|
|
}
|
|
|
|
void KGPGFile::slotErrorFromGPG(KProcess *, char *buf, int len)
|
|
{
|
|
// tqDebug("Received %d bytes on stderr", len);
|
|
TQCString msg;
|
|
msg.setRawData(buf, len);
|
|
m_errmsg += msg;
|
|
msg.resetRawData(buf, len);
|
|
}
|
|
|
|
void KGPGFile::slotSendDataToGPG(KProcess *)
|
|
{
|
|
// tqDebug("wrote stdin");
|
|
if(m_needExitLoop) {
|
|
m_needExitLoop = false;
|
|
tqApp->exit_loop();
|
|
}
|
|
}
|
|
|
|
bool KGPGFile::GPGAvailable(void)
|
|
{
|
|
TQString output;
|
|
char buffer[1024];
|
|
TQ_LONG len;
|
|
|
|
KGPGFile file;
|
|
file.open(IO_ReadOnly, "--version", true);
|
|
while((len = file.readBlock(buffer, sizeof(buffer)-1)) != EOF) {
|
|
buffer[len] = 0;
|
|
output += TQString(buffer);
|
|
}
|
|
file.close();
|
|
return !output.isEmpty();
|
|
}
|
|
|
|
bool KGPGFile::keyAvailable(const TQString& name)
|
|
{
|
|
TQStringList list;
|
|
publicKeyList(list, name);
|
|
return !list.isEmpty();
|
|
}
|
|
|
|
void KGPGFile::publicKeyList(TQStringList& list, const TQString& pattern)
|
|
{
|
|
TQMap<TQString, TQString> map;
|
|
TQString output;
|
|
char buffer[1024];
|
|
TQ_LONG len;
|
|
|
|
list.clear();
|
|
KGPGFile file;
|
|
TQString args("--list-keys --with-colons");
|
|
if(!pattern.isEmpty())
|
|
args += TQString(" %1").arg(pattern);
|
|
file.open(IO_ReadOnly, args, true);
|
|
while((len = file.readBlock(buffer, sizeof(buffer)-1)) != EOF) {
|
|
buffer[len] = 0;
|
|
output += TQString(buffer);
|
|
}
|
|
file.close();
|
|
|
|
// now parse the data. it looks like:
|
|
/*
|
|
tru::0:1210616414:1214841688:3:1:5
|
|
pub:u:1024:17:9C59DB40B75DD3BA:2001-06-23:::u:Thomas Baumgart <thomas.baumgart@syrocon.de>::scaESCA:
|
|
uid:u::::2001-11-29::63493BF182C494227E198FE5DA00ACDF63961AFB::Thomas Baumgart <thb@net-bembel.de>:
|
|
uid:u::::2001-11-29::00A393737BC120C98A6402B921599F6D72058DD8::Thomas Baumgart <ipwizard@users.sourceforge.net>:
|
|
sub:u:1024:16:85968A70D1F83C2B:2001-06-23::::::e:
|
|
*/
|
|
TQStringList lines = TQStringList::split("\n", output);
|
|
TQStringList::iterator it;
|
|
TQString currentKey;
|
|
for(it = lines.begin(); it != lines.end(); ++it) {
|
|
// tqDebug("Parsing: '%s'", (*it).data());
|
|
TQStringList fields = TQStringList::split(":", (*it), true);
|
|
TQString val;
|
|
if(fields[0] == "pub") {
|
|
TQDate expiration = TQDate::fromString(fields[6], Qt::ISODate);
|
|
if(expiration > TQDate::currentDate()) {
|
|
currentKey = fields[4];
|
|
val = TQString("%1:%2").arg(currentKey).arg(fields[9]);
|
|
map[val] = val;
|
|
} else {
|
|
tqDebug("'%s' is expired", fields[9].data());
|
|
}
|
|
} else if(fields[0] == "uid") {
|
|
val = TQString("%1:%2").arg(currentKey).arg(fields[9]);
|
|
map[val] = val;
|
|
}
|
|
}
|
|
list = map.values();
|
|
}
|
|
|
|
|
|
void KGPGFile::secretKeyList(TQStringList& list)
|
|
{
|
|
TQString output;
|
|
char buffer[1024];
|
|
TQ_LONG len;
|
|
|
|
list.clear();
|
|
KGPGFile file;
|
|
file.open(IO_ReadOnly, "--list-secret-keys --with-colons", true);
|
|
while((len = file.readBlock(buffer, sizeof(buffer)-1)) != EOF) {
|
|
buffer[len] = 0;
|
|
output += TQString(buffer);
|
|
}
|
|
file.close();
|
|
|
|
// now parse the data. it looks like:
|
|
/*
|
|
sec::1024:17:9C59DB40B75DD3BA:2001-06-23::::Thomas Baumgart <ipwizard@users.sourceforge.net>:::
|
|
uid:::::::::Thomas Baumgart <thb@net-bembel.de>:
|
|
ssb::1024:16:85968A70D1F83C2B:2001-06-23:::::::
|
|
sec::1024:17:59B0F826D2B08440:2005-01-03:2010-01-02:::KMyMoney emergency data recovery <kmymoney-recover@users.sourceforge.net>:::
|
|
ssb::2048:16:B3DABDC48C0FE2F3:2005-01-03:::::::
|
|
*/
|
|
TQStringList lines = TQStringList::split("\n", output);
|
|
TQStringList::iterator it;
|
|
TQString currentKey;
|
|
for(it = lines.begin(); it != lines.end(); ++it) {
|
|
// tqDebug("Parsing: '%s'", (*it).data());
|
|
TQStringList fields = TQStringList::split(":", (*it), true);
|
|
if(fields[0] == "sec") {
|
|
currentKey = fields[4];
|
|
list << TQString("%1:%2").arg(currentKey).arg(fields[9]);
|
|
} else if(fields[0] == "uid") {
|
|
list << TQString("%1:%2").arg(currentKey).arg(fields[9]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
// key generation
|
|
char * gpg_input =
|
|
g_strdup_printf("Key-Type: DSA\n"
|
|
"Key-Length: 1024\n"
|
|
"Subkey-Type: ELG-E\n"
|
|
"Subkey-Length: 1024\n"
|
|
"Name-Real: %s\n"
|
|
"Name-Comment: %s\n"
|
|
"Name-Email: %s\n"
|
|
"Passphrase: %s\n"
|
|
"%%commit\n",
|
|
username ? username : "",
|
|
idstring ? idstring : "",
|
|
email ? email : "",
|
|
passphrase ? passphrase : "");
|
|
char * argv [] =
|
|
{ "gpg",
|
|
"--batch",
|
|
"-q",
|
|
"--gen-key",
|
|
"--keyring",
|
|
"~/.gnucash/gnucash.pub",
|
|
"--secret-keyring",
|
|
"~/.gnucash/gnucash.sec",
|
|
NULL
|
|
};
|
|
|
|
char * retval = gnc_gpg_transform(gpg_input, strlen(gpg_input), NULL, argv);
|
|
g_free(gpg_input);
|
|
return retval;
|
|
|
|
*/
|
|
|
|
#if KMM_DEBUG
|
|
void KGPGFile::dumpBuffer(char *s, int len) const
|
|
{
|
|
TQString data, tmp, chars;
|
|
unsigned long addr = 0x0;
|
|
|
|
while(1) {
|
|
if(addr && !(addr & 0x0f)) {
|
|
tqDebug("%s %s", data.data(), chars.data());
|
|
if(!len)
|
|
break;
|
|
}
|
|
if(!(addr & 0x0f)) {
|
|
data = tmp.sprintf("%08lX", addr);
|
|
chars = TQString();
|
|
}
|
|
if(!(addr & 0x03)) {
|
|
data += " ";
|
|
}
|
|
++addr;
|
|
|
|
if(!len) {
|
|
data += " ";
|
|
chars += " ";
|
|
continue;
|
|
}
|
|
|
|
data += tmp.sprintf("%02X", *s & 0xff);
|
|
if(*s >= ' ' && *s <= '~')
|
|
chars += *s & 0xff;
|
|
else
|
|
chars += '.';
|
|
++s;
|
|
--len;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#include "kgpgfile.moc"
|