File indexing completed on 2024-04-28 04:57:49

0001 /*  This file is part of the KDE libraries
0002     SPDX-FileCopyrightText: 2000 Matthias Hoelzer-Kluepfel <mhk@caldera.de>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "kio_man.h"
0008 #include "kio_man_debug.h"
0009 
0010 #include <QByteArray>
0011 #include <QCoreApplication>
0012 #include <QDir>
0013 #include <QFile>
0014 #include <QMap>
0015 #include <QProcess>
0016 #include <QRegularExpression>
0017 #include <QStandardPaths>
0018 #include <QTextCodec>
0019 #include <QTextStream>
0020 #include <QUrl>
0021 
0022 #include <KLocalizedString>
0023 
0024 #include "man2html.h"
0025 #include <KCompressionDevice>
0026 
0027 using namespace KIO;
0028 
0029 // Pseudo plugin class to embed meta data
0030 class KIOPluginForMetaData : public QObject
0031 {
0032     Q_OBJECT
0033     Q_PLUGIN_METADATA(IID "org.kde.kio.worker.man" FILE "man.json")
0034 };
0035 
0036 MANProtocol *MANProtocol::s_self = nullptr;
0037 
0038 static const char *SGML2ROFF_DIRS = "/usr/lib/sgml";
0039 static const char *SGML2ROFF_EXECUTABLE = "sgml2roff";
0040 
0041 /*
0042  * Drop trailing compression suffix from name
0043  */
0044 static QString stripCompression(const QString &name)
0045 {
0046     int pos = name.length();
0047 
0048     if (name.endsWith(".gz"))
0049         pos -= 3;
0050     else if (name.endsWith(".z", Qt::CaseInsensitive))
0051         pos -= 2;
0052     else if (name.endsWith(".bz2"))
0053         pos -= 4;
0054     else if (name.endsWith(".bz"))
0055         pos -= 3;
0056     else if (name.endsWith(".lzma"))
0057         pos -= 5;
0058     else if (name.endsWith(".xz"))
0059         pos -= 3;
0060     else if (name.endsWith(".zst"))
0061         pos -= 4;
0062     else if (name.endsWith(".br"))
0063         pos -= 3;
0064 
0065     return (pos > 0 ? name.left(pos) : name);
0066 }
0067 
0068 /*
0069  * Drop trailing compression suffix and section from name
0070  */
0071 static QString stripExtension(const QString &name)
0072 {
0073     QString wc = stripCompression(name);
0074     const int pos = wc.lastIndexOf('.');
0075     return (pos > 0 ? wc.left(pos) : wc);
0076 }
0077 
0078 static bool parseUrl(const QString &_url, QString &title, QString &section)
0079 {
0080     section.clear();
0081 
0082     QString url = _url.trimmed();
0083     if (url.isEmpty() || url.startsWith('/')) {
0084         if (url.isEmpty() || QFile::exists(url)) {
0085             // man:/usr/share/man/man1/ls.1.gz is a valid file
0086             title = url;
0087             return true;
0088         } else {
0089             // If a full path is specified but does not exist,
0090             // then it is perhaps a normal man page.
0091             qCDebug(KIO_MAN_LOG) << url << " does not exist";
0092         }
0093     }
0094 
0095     while (url.startsWith('/'))
0096         url.remove(0, 1);
0097     title = url;
0098 
0099     int pos = url.indexOf('(');
0100     if (pos < 0)
0101         return true; // man:ls -> title=ls
0102 
0103     title = title.left(pos);
0104     section = url.mid(pos + 1);
0105 
0106     pos = section.indexOf(')');
0107     if (pos >= 0) {
0108         if (pos < section.length() - 2 && title.isEmpty()) {
0109             title = section.mid(pos + 2);
0110         }
0111         section = section.left(pos);
0112     }
0113 
0114     // man:ls(2) -> title="ls", section="2"
0115 
0116     return true;
0117 }
0118 
0119 MANProtocol::MANProtocol(const QByteArray &pool_socket, const QByteArray &app_socket)
0120     : QObject()
0121     , WorkerBase("man", pool_socket, app_socket)
0122 {
0123     Q_ASSERT(s_self == nullptr);
0124     s_self = this;
0125 
0126     m_sectionNames << "0"
0127                    << "0p"
0128                    << "1"
0129                    << "1p"
0130                    << "2"
0131                    << "3"
0132                    << "3n"
0133                    << "3p"
0134                    << "4"
0135                    << "5"
0136                    << "6"
0137                    << "7"
0138                    << "8"
0139                    << "9"
0140                    << "l"
0141                    << "n";
0142 
0143     const QString cssPath(QStandardPaths::locate(QStandardPaths::GenericDataLocation, "kio_docfilter/kio_docfilter.css"));
0144     m_manCSSFile = QFile::encodeName(QUrl::fromLocalFile(cssPath).url());
0145 }
0146 
0147 MANProtocol *MANProtocol::self()
0148 {
0149     return s_self;
0150 }
0151 
0152 MANProtocol::~MANProtocol()
0153 {
0154     s_self = nullptr;
0155 }
0156 
0157 void MANProtocol::parseWhatIs(QMap<QString, QString> &i, QTextStream &t, const QString &mark)
0158 {
0159     const QRegularExpression re(mark);
0160     QString l;
0161     while (!t.atEnd()) {
0162         l = t.readLine();
0163         QRegularExpressionMatch match = re.match(l);
0164         int pos = match.capturedStart(0);
0165         if (pos != -1) {
0166             QString names = l.left(pos);
0167             QString descr = l.mid(match.capturedEnd(0));
0168             while ((pos = names.indexOf(",")) != -1) {
0169                 i[names.left(pos++)] = descr;
0170                 while (names[pos] == ' ')
0171                     pos++;
0172                 names = names.mid(pos);
0173             }
0174             i[names] = descr;
0175         }
0176     }
0177 }
0178 
0179 bool MANProtocol::addWhatIs(QMap<QString, QString> &i, const QString &name, const QString &mark)
0180 {
0181     QFile f(name);
0182     if (!f.open(QIODevice::ReadOnly))
0183         return false;
0184 
0185     QTextStream t(&f);
0186     parseWhatIs(i, t, mark);
0187     return true;
0188 }
0189 
0190 QMap<QString, QString> MANProtocol::buildIndexMap(const QString &section)
0191 {
0192     qCDebug(KIO_MAN_LOG) << "for section" << section;
0193 
0194     QMap<QString, QString> i;
0195     QStringList man_dirs = manDirectories();
0196     // Supplementary places for whatis databases
0197     man_dirs += m_mandbpath;
0198     if (!man_dirs.contains("/var/cache/man"))
0199         man_dirs << "/var/cache/man";
0200     if (!man_dirs.contains("/var/catman"))
0201         man_dirs << "/var/catman";
0202 
0203     QStringList names;
0204     names << "whatis.db"
0205           << "whatis";
0206     const QString mark = "\\s+\\(" + section + "[a-z]*\\)\\s+-\\s+";
0207 
0208     int count0;
0209     for (const QString &it_dir : qAsConst(man_dirs)) {
0210         if (!QFile::exists(it_dir))
0211             continue;
0212 
0213         bool added = false;
0214         for (const QString &it_name : qAsConst(names)) {
0215             count0 = i.count();
0216             if (addWhatIs(i, (it_dir + '/' + it_name), mark)) {
0217                 qCDebug(KIO_MAN_LOG) << "added" << (i.count() - count0) << "from" << it_name << "in" << it_dir;
0218                 added = true;
0219                 break;
0220             }
0221         }
0222 
0223         if (!added) {
0224             // Nothing was able to be added by scanning the directory,
0225             // so try parsing the output of the whatis(1) command.
0226             QProcess proc;
0227             proc.setProgram("whatis");
0228             proc.setArguments(QStringList() << "-M" << it_dir << "-w"
0229                                             << "*");
0230             proc.setProcessChannelMode(QProcess::ForwardedErrorChannel);
0231             proc.start();
0232             proc.waitForFinished();
0233             QTextStream t(proc.readAllStandardOutput(), QIODevice::ReadOnly);
0234 
0235             count0 = i.count();
0236             parseWhatIs(i, t, mark);
0237             qCDebug(KIO_MAN_LOG) << "added" << (i.count() - count0) << "from whatis in" << it_dir;
0238         }
0239     }
0240 
0241     qCDebug(KIO_MAN_LOG) << "returning" << i.count() << "index entries";
0242     return i;
0243 }
0244 
0245 //---------------------------------------------------------------------
0246 
0247 QStringList MANProtocol::manDirectories()
0248 {
0249     checkManPaths();
0250     //
0251     // Build a list of man directories including translations
0252     //
0253     QStringList man_dirs;
0254     const QList<QLocale> locales = QLocale::matchingLocales(QLocale::AnyLanguage, QLocale::AnyScript, QLocale::AnyCountry);
0255 
0256     for (const QString &it_dir : qAsConst(m_manpath)) {
0257         // Translated pages in "<mandir>/<lang>" if the directory
0258         // exists
0259         for (const QLocale &it_loc : locales) {
0260             // TODO: languageToString() is wrong, that returns the readable name
0261             // of the language.  We want the country code returned by name().
0262             QString lang = QLocale::languageToString(it_loc.language());
0263             if (!lang.isEmpty() && lang != QString("C")) {
0264                 QString dir = it_dir + '/' + lang;
0265                 QDir d(dir);
0266                 if (d.exists()) {
0267                     const QString p = d.canonicalPath();
0268                     if (!man_dirs.contains(p))
0269                         man_dirs += p;
0270                 }
0271             }
0272         }
0273 
0274         // Untranslated pages in "<mandir>"
0275         const QString p = QDir(it_dir).canonicalPath();
0276         if (!man_dirs.contains(p))
0277             man_dirs += p;
0278     }
0279 
0280     qCDebug(KIO_MAN_LOG) << "returning" << man_dirs.count() << "man directories";
0281     return man_dirs;
0282 }
0283 
0284 QStringList MANProtocol::findPages(const QString &_section, const QString &title, bool full_path)
0285 {
0286     QStringList list;
0287     // qCDebug(KIO_MAN_LOG) << "findPages '" << section << "' '" << title << "'\n";
0288     if (title.startsWith('/')) // absolute man page path
0289     {
0290         list.append(title); // just that and nothing else
0291         return list;
0292     }
0293 
0294     const QString star("*"); // flag for finding sections
0295     const QString man("man"); // prefix for ROFF subdirectories
0296     const QString sman("sman"); // prefix for SGML subdirectories
0297 
0298     // Generate a list of applicable sections
0299     QStringList sect_list;
0300     if (!_section.isEmpty()) // section requested as parameter
0301     {
0302         sect_list += _section;
0303 
0304         // It's not clear what this code does.  If a section with a letter
0305         // suffix is specified, for example "3p", both "3p" and "3" are
0306         // added to the section list.  The result is that "man:(3p)" returns
0307         // the same entries as "man:(3)"
0308         QString section = _section;
0309         while ((!section.isEmpty()) && (section.at(section.length() - 1).isLetter())) {
0310             section.truncate(section.length() - 1);
0311             sect_list += section;
0312         }
0313     } else {
0314         sect_list += star;
0315     }
0316 
0317     const QStringList man_dirs = manDirectories();
0318     QStringList nameFilters;
0319     nameFilters += man + '*';
0320     nameFilters += sman + '*';
0321 
0322     // Find man pages in the sections specified above,
0323     // or all sections.
0324     //
0325     // Do not convert this loop to an iterator, or 'it_s' below
0326     // to a reference.  The 'sect_list' list can be modified
0327     // within the loop.
0328     for (int i = 0; i < sect_list.count(); ++i) {
0329         const QString it_s = sect_list.at(i);
0330         QString it_real = it_s.toLower();
0331 
0332         // Find applicable pages within all man directories.
0333         for (const QString &man_dir : man_dirs) {
0334             // Find all subdirectories named "man*" and "sman*"
0335             // to extend the list of sections and find the correct
0336             // case for the section suffix.
0337             QDir dp(man_dir);
0338             dp.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
0339             dp.setNameFilters(nameFilters);
0340             const QStringList entries = dp.entryList();
0341             for (const QString &file : entries) {
0342                 QString sect;
0343                 if (file.startsWith(man))
0344                     sect = file.mid(man.length());
0345                 else if (file.startsWith(sman))
0346                     sect = file.mid(sman.length());
0347                 // Should never happen, because of the name filter above.
0348                 if (sect.isEmpty())
0349                     continue;
0350 
0351                 if (sect.toLower() == it_real)
0352                     it_real = sect;
0353 
0354                 // Only add sect if not already contained, avoid duplicates
0355                 if (!sect_list.contains(sect) && _section.isEmpty()) {
0356                     // qCDebug(KIO_MAN_LOG) << "another section " << sect;
0357                     sect_list += sect;
0358                 }
0359             }
0360 
0361             if (it_s != star) // finding pages, not just sections
0362             {
0363                 const QString dir = man_dir + '/' + man + it_real + '/';
0364                 list.append(findManPagesInSection(dir, title, full_path));
0365 
0366                 const QString sdir = man_dir + '/' + sman + it_real + '/';
0367                 list.append(findManPagesInSection(sdir, title, full_path));
0368             }
0369         }
0370     }
0371 
0372     // qCDebug(KIO_MAN_LOG) << "finished " << list << " " << sect_list;
0373     return list;
0374 }
0375 
0376 QStringList MANProtocol::findManPagesInSection(const QString &dir, const QString &title, bool full_path)
0377 {
0378     QStringList list;
0379 
0380     qCDebug(KIO_MAN_LOG) << "in" << dir << "title" << title;
0381     const bool title_given = !title.isEmpty();
0382 
0383     QDir dp(dir);
0384     dp.setFilter(QDir::Files);
0385     const QStringList names = dp.entryList();
0386     for (const QString &name : names) {
0387         if (title_given) {
0388             // check title if we're looking for a specific page
0389             if (!name.startsWith(title))
0390                 continue;
0391             // beginning matches, do a more thorough check...
0392             const QString tmp_name = stripExtension(name);
0393             if (tmp_name != title)
0394                 continue;
0395         }
0396 
0397         list.append(full_path ? dir + name : name);
0398     }
0399 
0400     qCDebug(KIO_MAN_LOG) << "returning" << list.count() << "pages";
0401     return (list);
0402 }
0403 
0404 //---------------------------------------------------------------------
0405 
0406 void MANProtocol::output(const char *insert)
0407 {
0408     if (insert) {
0409         m_outputBuffer.write(insert, strlen(insert));
0410     }
0411     if (!insert || m_outputBuffer.pos() >= 2048) {
0412         m_outputBuffer.close();
0413         data(m_outputBuffer.buffer());
0414         m_outputBuffer.setData(QByteArray());
0415         m_outputBuffer.open(QIODevice::WriteOnly);
0416     }
0417 }
0418 
0419 #ifndef SIMPLE_MAN2HTML
0420 // called by man2html
0421 extern char *read_man_page(const char *filename)
0422 {
0423     return MANProtocol::self()->readManPage(filename);
0424 }
0425 
0426 // called by man2html
0427 extern void output_real(const char *insert)
0428 {
0429     MANProtocol::self()->output(insert);
0430 }
0431 #endif
0432 
0433 //---------------------------------------------------------------------
0434 
0435 KIO::WorkerResult MANProtocol::get(const QUrl &url)
0436 {
0437     qCDebug(KIO_MAN_LOG) << "GET " << url.url();
0438 
0439     // Indicate the mimetype - this will apply to both
0440     // formatted man pages and error pages.
0441     mimeType("text/html");
0442 
0443     QString title, section;
0444     if (!parseUrl(url.path(), title, section)) {
0445         showMainIndex();
0446         return KIO::WorkerResult::pass();
0447     }
0448 
0449     // see if an index was requested
0450     if (url.query().isEmpty() && (title.isEmpty() || title == "/" || title == ".")) {
0451         if (section == "index" || section.isEmpty())
0452             showMainIndex();
0453         else
0454             showIndex(section);
0455         return KIO::WorkerResult::pass();
0456     }
0457 
0458     QStringList foundPages = findPages(section, title);
0459     if (foundPages.isEmpty()) {
0460         outputError(xi18nc("@info",
0461                            "No man page matching <resource>%1</resource> could be found."
0462                            "<nl/><nl/>"
0463                            "Check that you have not mistyped the name of the page, "
0464                            "and note that man page names are case sensitive."
0465                            "<nl/><nl/>"
0466                            "If the name is correct, then you may need to extend the search path "
0467                            "for man pages, either using the <envar>MANPATH</envar> environment "
0468                            "variable or a configuration file in the <filename>/etc</filename> "
0469                            "directory.",
0470                            title.toHtmlEscaped()));
0471         return KIO::WorkerResult::pass();
0472     }
0473 
0474     // Sort the list of pages now, for display if required and for
0475     // testing for equivalents below.
0476     std::sort(foundPages.begin(), foundPages.end());
0477     const QString pageFound = foundPages.first();
0478 
0479     if (foundPages.count() > 1) {
0480         // See if the multiple pages found refer to the same man page, for example
0481         // if 'foo.1' and 'foo.1.gz' were both found.  To make this generic with
0482         // regard to compression suffixes, assume that the first page name (after
0483         // the list has been sorted above) is the shortest.  Then check that all of
0484         // the others are the same with a possible compression suffix added.
0485         for (int i = 1; i < foundPages.count(); ++i) {
0486             if (!foundPages[i].startsWith(pageFound + '.')) {
0487                 // There is a page which is not the same as the reference, even
0488                 // allowing for a compression suffix.  Output the list of multiple
0489                 // pages only.
0490                 outputMatchingPages(foundPages);
0491                 return KIO::WorkerResult::pass();
0492             }
0493         }
0494     }
0495 
0496     setCssFile(m_manCSSFile);
0497     m_outputBuffer.open(QIODevice::WriteOnly);
0498     const QByteArray filename = QFile::encodeName(pageFound);
0499     const char *buf = readManPage(filename);
0500     if (!buf) {
0501         // readManPage emits an error page instead, so still passing
0502         return KIO::WorkerResult::pass();
0503         ;
0504     }
0505 
0506     // will call output_real
0507     scan_man_page(buf);
0508     delete[] buf;
0509 
0510     output(nullptr); // flush
0511 
0512     m_outputBuffer.close();
0513     data(m_outputBuffer.buffer());
0514     m_outputBuffer.setData(QByteArray());
0515 
0516     // tell we are done
0517     data(QByteArray());
0518     return KIO::WorkerResult::pass();
0519 }
0520 
0521 //---------------------------------------------------------------------
0522 
0523 // If this function returns nullptr to indicate a problem,
0524 // then it must call outputError() first.
0525 char *MANProtocol::readManPage(const char *_filename)
0526 {
0527     QByteArray filename = _filename;
0528     QByteArray array, dirName;
0529 
0530     // Determine the type of man page file by checking its path
0531     // Determination by MIME type with KMimeType doesn't work reliably.
0532     // E.g., Solaris 7: /usr/man/sman7fs/pcfs.7fs -> text/x-csrc - WRONG
0533     //
0534     // If the path name contains a component "sman", assume that it's SGML
0535     // and convert it to roff format (used on Solaris).  Check for a pathname
0536     // component of "sman" only - we don't want a man page called "gasman.1"
0537     // to match.
0538     // QString file_mimetype = KMimeType::findByPath(QString(filename), 0, false)->name();
0539 
0540     if (QString(filename).contains("/sman/", Qt::CaseInsensitive)) {
0541         QProcess proc;
0542         // Determine path to sgml2roff, if not already done.
0543         if (!getProgramPath())
0544             return nullptr;
0545         proc.setProgram(mySgml2RoffPath);
0546         proc.setArguments(QStringList() << filename);
0547         proc.setProcessChannelMode(QProcess::ForwardedErrorChannel);
0548         proc.start();
0549         proc.waitForFinished();
0550         array = proc.readAllStandardOutput();
0551     } else {
0552         if (QDir::isRelativePath(filename)) {
0553             qCDebug(KIO_MAN_LOG) << "relative" << filename;
0554             filename = QDir::cleanPath(lastdir + '/' + filename).toUtf8();
0555             qCDebug(KIO_MAN_LOG) << "resolved to" << filename;
0556         }
0557 
0558         lastdir = filename.left(filename.lastIndexOf('/'));
0559 
0560         // get the last directory name (which might be a language name, to be able to guess the encoding)
0561         QDir dir(lastdir);
0562         dir.cdUp();
0563         dirName = QFile::encodeName(dir.dirName());
0564 
0565         if (!QFile::exists(QFile::decodeName(filename))) // if given file does not exist, find with suffix
0566         {
0567             qCDebug(KIO_MAN_LOG) << "not existing " << filename;
0568             QDir mandir(lastdir);
0569             const QString nameFilter = filename.mid(filename.lastIndexOf('/') + 1) + ".*";
0570             mandir.setNameFilters(QStringList(nameFilter));
0571 
0572             const QStringList entries = mandir.entryList();
0573             if (entries.isEmpty()) {
0574                 outputError(xi18nc("@info",
0575                                    "The specified man page references "
0576                                    "another page <filename>%1</filename>,"
0577                                    "<nl/>"
0578                                    "but the referenced page <filename>%2</filename> "
0579                                    "could not be found.",
0580                                    QFile::decodeName(filename),
0581                                    QDir::cleanPath(lastdir + '/' + nameFilter)));
0582                 return nullptr;
0583             }
0584 
0585             filename = lastdir + '/' + QFile::encodeName(entries.first());
0586             qCDebug(KIO_MAN_LOG) << "resolved to " << filename;
0587         }
0588 
0589         KCompressionDevice fd(QFile::encodeName(filename));
0590 
0591         if (!fd.open(QIODevice::ReadOnly)) {
0592             outputError(xi18nc("@info", "The man page <filename>%1</filename> could not be read.", QFile::decodeName(filename)));
0593             return nullptr;
0594         }
0595 
0596         array = fd.readAll();
0597         qCDebug(KIO_MAN_LOG) << "read " << array.size();
0598     }
0599 
0600     if (array.isEmpty()) {
0601         outputError(xi18nc("@info", "The man page <filename>%1</filename> could not be converted.", QFile::decodeName(filename)));
0602         return nullptr;
0603     }
0604 
0605     return manPageToUtf8(array, dirName); // never returns nullptr
0606 }
0607 
0608 //---------------------------------------------------------------------
0609 
0610 // This opens one <div> to format the HTML body, so when the document
0611 // is finished it needs to be closed by outputFooter().
0612 void MANProtocol::outputHeader(QTextStream &os, const QString &header, const QString &title)
0613 {
0614     const QString pageTitle = (!title.isEmpty() ? title : header);
0615 
0616 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0617     os.setCodec("UTF-8");
0618 #endif
0619 
0620     // The same header and styling as generated by man2html
0621     os << "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\n";
0622     os << "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n";
0623     os << "<title>" << pageTitle << "</title>\n";
0624     os << "<link rel=\"stylesheet\" href=\"help:/kdoctools5-common/kde-default.css\" type=\"text/css\">\n";
0625     if (!m_manCSSFile.isEmpty()) {
0626         os << "<link rel=\"stylesheet\" href=\"" << m_manCSSFile << "\" type=\"text/css\">\n";
0627     }
0628     os << "<style type=\"text/css\">\n";
0629     os << "#header_top { background-image: url(\"help:/kdoctools5-common/top.jpg\"); }\n";
0630     os << "#header_top div { background-image: url(\"help:/kdoctools5-common/top-left.jpg\"); }\n";
0631     os << "#header_top div div { background-image: url(\"help:/kdoctools5-common/top-right.jpg\"); }\n";
0632     os << "</style>\n";
0633     os << "</head>\n";
0634 
0635     os << "<body>\n";
0636     os << "<div id=\"header\"><div id=\"header_top\"><div><div>\n";
0637     os << "<img src=\"help:/kdoctools5-common/top-kde.jpg\" alt=\"top-kde\">\n";
0638     os << pageTitle << "\n";
0639     os << "</div></div></div></div>\n";
0640 
0641     os << "<div style=\"margin-left: 5em; margin-right: 5em;\">\n";
0642     os << "<h1>" << header << "</h1>\n";
0643 
0644     os.flush();
0645 }
0646 
0647 void MANProtocol::outputFooter(QTextStream &os)
0648 {
0649     os << "</div>\n"; // closes "<div style="margin-left: 5em; margin-right: 5em;"
0650     os << "</body>\n";
0651     os << "</html>\n";
0652     os.flush();
0653 }
0654 
0655 // Do not call any other WorkerBase functions afterwards,
0656 // only return a KIO::WorkerResult::pass().
0657 // It is assumed that mimeType() has already been called at the start of get().
0658 void MANProtocol::outputError(const QString &errmsg)
0659 {
0660     QByteArray array;
0661     QTextStream os(&array, QIODevice::WriteOnly);
0662 
0663     outputHeader(os, i18n("Manual Page Viewer Error"));
0664     os << errmsg << "\n";
0665     outputFooter(os);
0666 
0667     data(array);
0668     data(QByteArray());
0669 }
0670 
0671 void MANProtocol::outputMatchingPages(const QStringList &matchingPages)
0672 {
0673     QByteArray array;
0674     QTextStream os(&array, QIODevice::WriteOnly);
0675 
0676     outputHeader(os, i18n("There is more than one matching man page:"), i18n("Multiple Manual Pages"));
0677     os << "<ul>\n";
0678 
0679     int acckey = 1;
0680     for (const QString &page : matchingPages) {
0681         os << "<li><a href='man:" << page << "' accesskey='" << acckey << "'>" << page << "</a><br>\n<br>\n";
0682         ++acckey;
0683     }
0684 
0685     os << "</ul>\n";
0686     os << "<hr>\n";
0687     os << "<p>"
0688        << i18n(
0689               "Note: if you read a man page in your language,"
0690               " be aware it can contain some mistakes or be obsolete."
0691               " In case of doubt, you should have a look at the English version.")
0692        << "</p>";
0693 
0694     outputFooter(os);
0695     data(array);
0696     // Do not call finished(), the caller will do that
0697 }
0698 
0699 KIO::WorkerResult MANProtocol::stat(const QUrl &url)
0700 {
0701     qCDebug(KIO_MAN_LOG) << "STAT " << url.url();
0702 
0703     QString title, section;
0704 
0705     if (!parseUrl(url.path(), title, section)) {
0706         return KIO::WorkerResult::fail(KIO::ERR_MALFORMED_URL, url.url());
0707     }
0708 
0709     qCDebug(KIO_MAN_LOG) << "URL" << url.url() << "parsed to title" << title << "section" << section;
0710 
0711     UDSEntry entry;
0712     entry.reserve(3);
0713     entry.fastInsert(KIO::UDSEntry::UDS_NAME, title);
0714     entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFREG);
0715     entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("text/html"));
0716 
0717     statEntry(entry);
0718 
0719     return KIO::WorkerResult::pass();
0720 }
0721 
0722 extern "C" {
0723 
0724 int Q_DECL_EXPORT kdemain(int argc, char **argv)
0725 {
0726     QCoreApplication app(argc, argv);
0727     app.setApplicationName(QLatin1String("kio_man"));
0728 
0729     qCDebug(KIO_MAN_LOG) << "STARTING";
0730 
0731     if (argc != 4) {
0732         fprintf(stderr, "Usage: kio_man protocol domain-socket1 domain-socket2\n");
0733         exit(-1);
0734     }
0735 
0736     MANProtocol worker(argv[2], argv[3]);
0737     worker.dispatchLoop();
0738 
0739     qCDebug(KIO_MAN_LOG) << "Done";
0740 
0741     return 0;
0742 }
0743 }
0744 
0745 KIO::WorkerResult MANProtocol::mimetype(const QUrl & /*url*/)
0746 {
0747     mimeType("text/html");
0748 
0749     return KIO::WorkerResult::pass();
0750 }
0751 
0752 //---------------------------------------------------------------------
0753 
0754 static QString sectionName(const QString &section)
0755 {
0756     if (section == "0")
0757         return i18n("Header Files");
0758     else if (section == "0p")
0759         return i18n("Header Files (POSIX)");
0760     else if (section == "1")
0761         return i18n("User Commands");
0762     else if (section == "1p")
0763         return i18n("User Commands (POSIX)");
0764     else if (section == "2")
0765         return i18n("System Calls");
0766     else if (section == "3")
0767         return i18n("Subroutines");
0768     // TODO: current usage of '3p' seems to be "Subroutines (POSIX)"
0769     else if (section == "3p")
0770         return i18n("Perl Modules");
0771     else if (section == "3n")
0772         return i18n("Network Functions");
0773     else if (section == "4")
0774         return i18n("Devices");
0775     else if (section == "5")
0776         return i18n("File Formats");
0777     else if (section == "6")
0778         return i18n("Games");
0779     else if (section == "7")
0780         return i18n("Miscellaneous");
0781     else if (section == "8")
0782         return i18n("System Administration");
0783     else if (section == "9")
0784         return i18n("Kernel");
0785     else if (section == "l")
0786         return i18n("Local Documentation");
0787     // TODO: current usage of 'n' seems to be TCL commands
0788     else if (section == "n")
0789         return i18n("New");
0790 
0791     return QString();
0792 }
0793 
0794 QStringList MANProtocol::buildSectionList(const QStringList &dirs) const
0795 {
0796     QStringList l;
0797     for (const QString &it_sect : qAsConst(m_sectionNames)) {
0798         for (const QString &it_dir : dirs) {
0799             QDir d(it_dir + "/man" + it_sect);
0800             if (d.exists()) {
0801                 l << it_sect;
0802                 break;
0803             }
0804         }
0805     }
0806     return l;
0807 }
0808 
0809 //---------------------------------------------------------------------
0810 
0811 void MANProtocol::showMainIndex()
0812 {
0813     QByteArray array;
0814     QTextStream os(&array, QIODevice::WriteOnly);
0815 
0816     outputHeader(os, i18n("Main Manual Page Index"));
0817 
0818     // ### TODO: why still the environment variable
0819     // if keeping it, also use it in listDir()
0820     const QString sectList = qgetenv("MANSECT");
0821     QStringList sections;
0822     if (sectList.isEmpty())
0823         sections = buildSectionList(manDirectories());
0824     else
0825         sections = sectList.split(':');
0826 
0827     os << "<table>\n";
0828 
0829     QSet<QChar> accessKeys;
0830     char alternateAccessKey = 'a';
0831     for (const QString &it_sect : qAsConst(sections)) {
0832         if (it_sect.isEmpty())
0833             continue; // guard back() below
0834 
0835         // create a unique access key
0836         QChar accessKey = it_sect.back(); // rightmost char
0837         while (accessKeys.contains(accessKey))
0838             accessKey = alternateAccessKey++;
0839         accessKeys.insert(accessKey);
0840 
0841         os << "<tr><td><a href=\"man:(" << it_sect << ")\" accesskey=\"" << accessKey << "\">" << i18n("Section %1", it_sect) << "</a></td><td>&nbsp;</td><td> "
0842            << sectionName(it_sect) << "</td></tr>\n";
0843     }
0844 
0845     os << "</table>\n";
0846     outputFooter(os);
0847     data(array);
0848     data(QByteArray());
0849     // Do not call finished(), the caller will do that
0850 }
0851 
0852 //---------------------------------------------------------------------
0853 
0854 // TODO: constr_catmanpath modified here, should parameter be passed by reference?
0855 void MANProtocol::constructPath(QStringList &constr_path, QStringList constr_catmanpath)
0856 {
0857     QMap<QString, QString> manpath_map;
0858     QMap<QString, QString> mandb_map;
0859 
0860     // Add paths from /etc/man.conf
0861     //
0862     // Explicit manpaths may be given by lines starting with "MANPATH" or
0863     // "MANDATORY_MANPATH" (depending on system ?).
0864     // Mappings from $PATH to manpath are given by lines starting with
0865     // "MANPATH_MAP"
0866 
0867     // The entry is e.g. "MANDATORY_MANPATH    <manpath>"
0868     const QRegularExpression manpath_regex("^(?:MANPATH|MANDATORY_MANPATH)\\s+(\\S+)");
0869 
0870     // The entry is "MANPATH_MAP  <path>  <manpath>"
0871     const QRegularExpression manpath_map_regex("^MANPATH_MAP\\s+(\\S+)\\s+(\\S+)");
0872 
0873     // The entry is "MANDB_MAP  <manpath>  <catmanpath>"
0874     const QRegularExpression mandb_map_regex("^MANDB_MAP\\s+(\\S+)\\s+(\\S+)");
0875 
0876     QFile mc("/etc/man.conf"); // Caldera
0877     if (!mc.exists())
0878         mc.setFileName("/etc/manpath.config"); // SuSE, Debian
0879     if (!mc.exists())
0880         mc.setFileName("/etc/man.config"); // Mandrake
0881 
0882     if (mc.open(QIODevice::ReadOnly)) {
0883         QTextStream is(&mc);
0884         while (!is.atEnd()) {
0885             const QString line = is.readLine();
0886 
0887             QRegularExpressionMatch rmatch;
0888             if (line.contains(manpath_regex, &rmatch)) {
0889                 constr_path += rmatch.captured(1);
0890             } else if (line.contains(manpath_map_regex, &rmatch)) {
0891                 const QString dir = QDir::cleanPath(rmatch.captured(1));
0892                 const QString mandir = QDir::cleanPath(rmatch.captured(2));
0893                 manpath_map[dir] = mandir;
0894             } else if (line.contains(mandb_map_regex, &rmatch)) {
0895                 const QString mandir = QDir::cleanPath(rmatch.captured(1));
0896                 const QString catmandir = QDir::cleanPath(rmatch.captured(2));
0897                 mandb_map[mandir] = catmandir;
0898             }
0899             /* sections are not used
0900                     else if ( section_regex.find(line, 0) == 0 )
0901                     {
0902                     if ( !conf_section.isEmpty() )
0903                     conf_section += ':';
0904                     conf_section += line.mid(8).trimmed();
0905                 }
0906             */
0907         }
0908         mc.close();
0909     }
0910 
0911     // Default paths
0912     static const char *const manpaths[] = {"/usr/X11/man",     "/usr/X11R6/man", "/usr/man",          "/usr/local/man",     "/usr/exp/man",
0913                                            "/usr/openwin/man", "/usr/dt/man",    "/opt/freetool/man", "/opt/local/man",     "/usr/tex/man",
0914                                            "/usr/www/man",     "/usr/lang/man",  "/usr/gnu/man",      "/usr/share/man",     "/usr/motif/man",
0915                                            "/usr/titools/man", "/usr/sunpc/man", "/usr/ncd/man",      "/usr/newsprint/man", nullptr};
0916 
0917     int i = 0;
0918     while (manpaths[i]) {
0919         if (constr_path.indexOf(QString(manpaths[i])) == -1)
0920             constr_path += QString(manpaths[i]);
0921         i++;
0922     }
0923 
0924     // Directories in $PATH
0925     // - if a manpath mapping exists, use that mapping
0926     // - if a directory "<path>/man" or "<path>/../man" exists, add it
0927     //   to the man path (the actual existence check is done further down)
0928 
0929     if (::getenv("PATH")) {
0930         const QStringList path = QString::fromLocal8Bit(::getenv("PATH")).split(':', Qt::SkipEmptyParts);
0931 
0932         for (const QString &it : qAsConst(path)) {
0933             const QString dir = QDir::cleanPath(it);
0934             QString mandir = manpath_map[dir];
0935 
0936             if (!mandir.isEmpty()) {
0937                 // a path mapping exists
0938                 if (constr_path.indexOf(mandir) == -1)
0939                     constr_path += mandir;
0940             } else {
0941                 // no manpath mapping, use "<path>/man" and "<path>/../man"
0942 
0943                 mandir = dir + QString("/man");
0944                 if (constr_path.indexOf(mandir) == -1)
0945                     constr_path += mandir;
0946 
0947                 int pos = dir.lastIndexOf('/');
0948                 if (pos > 0) {
0949                     mandir = dir.left(pos) + QString("/man");
0950                     if (constr_path.indexOf(mandir) == -1)
0951                         constr_path += mandir;
0952                 }
0953             }
0954             QString catmandir = mandb_map[mandir];
0955             if (!mandir.isEmpty()) {
0956                 if (constr_catmanpath.indexOf(catmandir) == -1)
0957                     constr_catmanpath += catmandir;
0958             } else {
0959                 // What is the default mapping?
0960                 catmandir = mandir;
0961                 catmandir.replace("/usr/share/", "/var/cache/");
0962                 if (constr_catmanpath.indexOf(catmandir) == -1)
0963                     constr_catmanpath += catmandir;
0964             }
0965         }
0966     }
0967 }
0968 
0969 void MANProtocol::checkManPaths()
0970 {
0971     static bool inited = false;
0972     if (inited)
0973         return;
0974     inited = true;
0975 
0976     const QString manpath_env = qgetenv("MANPATH");
0977 
0978     // Decide if $MANPATH is enough on its own or if it should be merged
0979     // with the constructed path.
0980     // A $MANPATH starting or ending with ":", or containing "::",
0981     // should be merged with the constructed path.
0982 
0983     const bool construct_path = (manpath_env.isEmpty() || manpath_env.startsWith(':') || manpath_env.endsWith(':') || manpath_env.contains("::"));
0984 
0985     // Constructed man path -- consists of paths from
0986     //   /etc/man.conf
0987     //   default dirs
0988     //   $PATH
0989     QStringList constr_path;
0990     QStringList constr_catmanpath; // catmanpath
0991 
0992     if (construct_path) // need to read config file
0993     {
0994         constructPath(constr_path, constr_catmanpath);
0995     }
0996 
0997     m_mandbpath = constr_catmanpath;
0998 
0999     // Merge $MANPATH with the constructed path to form the
1000     // actual manpath.
1001     //
1002     // The merging syntax with ":" and "::" in $MANPATH will be
1003     // satisfied if any empty string in path_list_env (there
1004     // should be 1 or 0) is replaced by the constructed path.
1005 
1006     const QStringList path_list_env = manpath_env.split(':', Qt::KeepEmptyParts);
1007     for (const QString &dir : path_list_env) {
1008         if (!dir.isEmpty()) // non empty part - add if exists
1009         {
1010             if (m_manpath.contains(dir))
1011                 continue; // ignore if already present
1012             if (QDir(dir).exists())
1013                 m_manpath += dir; // add to list if exists
1014         } else // empty part - merge constructed path
1015         {
1016             // Insert constructed path ($MANPATH was empty, or
1017             // there was a ":" at either end or "::" within)
1018             for (const QString &dir2 : qAsConst(constr_path)) {
1019                 if (dir2.isEmpty())
1020                     continue; // don't add a null entry
1021                 if (m_manpath.contains(dir2))
1022                     continue; // ignore if already present
1023                 // add to list if exists
1024                 if (QDir(dir2).exists())
1025                     m_manpath += dir2;
1026             }
1027         }
1028     }
1029 
1030     /* sections are not used
1031         // Sections
1032         QStringList m_mansect = mansect_env.split( ':', Qt::KeepEmptyParts);
1033 
1034         const char* const default_sect[] =
1035             { "1", "2", "3", "4", "5", "6", "7", "8", "9", "n", 0L };
1036 
1037         for ( int i = 0; default_sect[i] != 0L; i++ )
1038             if ( m_mansect.indexOf( QString( default_sect[i] ) ) == -1 )
1039                 m_mansect += QString( default_sect[i] );
1040     */
1041 
1042     qCDebug(KIO_MAN_LOG) << "manpath" << m_manpath;
1043 }
1044 
1045 // Setup my own structure, with char pointers.
1046 // from now on only pointers are copied, no strings
1047 //
1048 // containing the whole path string,
1049 // the beginning of the man page name
1050 // and the length of the name
1051 struct man_index_t {
1052     char *manpath; // the full path including man file
1053     const char *manpage_begin; // pointer to the begin of the man file name in the path
1054     int manpage_len; // len of the man file name
1055 };
1056 typedef man_index_t *man_index_ptr;
1057 
1058 int compare_man_index(const void *s1, const void *s2)
1059 {
1060     struct man_index_t *m1 = *(struct man_index_t **)s1;
1061     struct man_index_t *m2 = *(struct man_index_t **)s2;
1062     int i;
1063     // Compare the names of the pages
1064     // with the shorter length.
1065     // Man page names are not '\0' terminated, so
1066     // this is a bit tricky
1067     if (m1->manpage_len > m2->manpage_len) {
1068         i = qstrnicmp(m1->manpage_begin, m2->manpage_begin, m2->manpage_len);
1069         if (!i)
1070             return 1;
1071         return i;
1072     }
1073 
1074     if (m1->manpage_len < m2->manpage_len) {
1075         i = qstrnicmp(m1->manpage_begin, m2->manpage_begin, m1->manpage_len);
1076         if (!i)
1077             return -1;
1078         return i;
1079     }
1080 
1081     return qstrnicmp(m1->manpage_begin, m2->manpage_begin, m1->manpage_len);
1082 }
1083 
1084 void MANProtocol::showIndex(const QString &section)
1085 {
1086     QByteArray array_h;
1087     QTextStream os_h(&array_h, QIODevice::WriteOnly);
1088 
1089     // print header
1090     outputHeader(os_h, i18n("Index for section %1: %2", section, sectionName(section)), i18n("Manual Page Index"));
1091 
1092     QByteArray array_d;
1093     QTextStream os(&array_d, QIODevice::WriteOnly);
1094 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
1095     os.setCodec("UTF-8");
1096 #endif
1097     os << "<div class=\"secidxmain\">\n";
1098 
1099     // compose list of search paths -------------------------------------------------------------
1100 
1101     checkManPaths();
1102     infoMessage(i18n("Generating Index"));
1103 
1104     // search for the man pages
1105     QStringList pages = findPages(section, QString());
1106 
1107     if (pages.isEmpty()) // not a single page found
1108     {
1109         // print footer
1110         outputFooter(os);
1111         os_h.flush();
1112 
1113         infoMessage(QString());
1114         data(array_h + array_d);
1115         return;
1116     }
1117 
1118     QMap<QString, QString> indexmap = buildIndexMap(section);
1119 
1120     // print out the list
1121     os << "<br/><br/>\n";
1122     os << "<table>\n";
1123 
1124     int listlen = pages.count();
1125     man_index_ptr *indexlist = new man_index_ptr[listlen];
1126     listlen = 0;
1127 
1128     QStringList::const_iterator page;
1129     for (const QString &page : qAsConst(pages)) {
1130         // I look for the beginning of the man page name
1131         // i.e. "bla/pagename.3.gz" by looking for the last "/"
1132         // Then look for the end of the name by searching backwards
1133         // for the last ".", not counting zip extensions.
1134         // If the len of the name is >0,
1135         // store it in the list structure, to be sorted later
1136 
1137         struct man_index_t *manindex = new man_index_t;
1138         manindex->manpath = strdup(page.toUtf8());
1139 
1140         manindex->manpage_begin = strrchr(manindex->manpath, '/');
1141         if (manindex->manpage_begin) {
1142             manindex->manpage_begin++;
1143             Q_ASSERT(manindex->manpage_begin >= manindex->manpath);
1144         } else {
1145             manindex->manpage_begin = manindex->manpath;
1146             Q_ASSERT(manindex->manpage_begin >= manindex->manpath);
1147         }
1148 
1149         // Skip extension ".section[.gz]"
1150 
1151         const char *begin = manindex->manpage_begin;
1152         const int len = strlen(begin);
1153         const char *end = begin + (len - 1);
1154 
1155         if (len >= 3 && strcmp(end - 2, ".gz") == 0)
1156             end -= 3;
1157         else if (len >= 2 && strcmp(end - 1, ".Z") == 0)
1158             end -= 2;
1159         else if (len >= 2 && strcmp(end - 1, ".z") == 0)
1160             end -= 2;
1161         else if (len >= 4 && strcmp(end - 3, ".bz2") == 0)
1162             end -= 4;
1163         else if (len >= 5 && strcmp(end - 4, ".lzma") == 0)
1164             end -= 5;
1165         else if (len >= 3 && strcmp(end - 2, ".xz") == 0)
1166             end -= 3;
1167 
1168         while (end >= begin && *end != '.')
1169             end--;
1170 
1171         if (end < begin) {
1172             // no '.' ending ???
1173             // set the pointer past the end of the filename
1174             manindex->manpage_len = page.length();
1175             manindex->manpage_len -= (manindex->manpage_begin - manindex->manpath);
1176             Q_ASSERT(manindex->manpage_len >= 0);
1177         } else {
1178             const char *manpage_end = end;
1179             manindex->manpage_len = (manpage_end - manindex->manpage_begin);
1180             Q_ASSERT(manindex->manpage_len >= 0);
1181         }
1182 
1183         if (manindex->manpage_len > 0) {
1184             indexlist[listlen] = manindex;
1185             listlen++;
1186         } else
1187             delete manindex;
1188     }
1189 
1190     //
1191     // Now do the sorting on the page names
1192     // and the printout afterwards
1193     // While printing avoid duplicate man page names
1194     //
1195 
1196     struct man_index_t dummy_index = {nullptr, nullptr, 0};
1197     struct man_index_t *last_index = &dummy_index;
1198 
1199     // sort and print
1200     qsort(indexlist, listlen, sizeof(struct man_index_t *), compare_man_index);
1201 
1202     QChar firstchar, tmp;
1203     QString indexLine = "<div class=\"secidxshort\">\n";
1204     if (indexlist[0]->manpage_len > 0) {
1205         firstchar = QChar((indexlist[0]->manpage_begin)[0]).toLower();
1206 
1207         const QString appendixstr = QString(" [<a href=\"#%1\" accesskey=\"%2\">%3</a>]\n").arg(firstchar).arg(firstchar).arg(firstchar);
1208         indexLine.append(appendixstr);
1209     }
1210     os << "<tr><td class=\"secidxnextletter\""
1211        << " colspan=\"3\">\n";
1212     os << "<a name=\"" << firstchar << "\">" << firstchar << "</a>\n";
1213     os << "</td></tr>\n";
1214 
1215     for (int i = 0; i < listlen; i++) {
1216         struct man_index_t *manindex = indexlist[i];
1217 
1218         // qstrncmp():
1219         // "last_man" has already a \0 string ending, but
1220         // "manindex->manpage_begin" has not,
1221         // so do compare at most "manindex->manpage_len" of the strings.
1222         if (last_index->manpage_len == manindex->manpage_len && !qstrncmp(last_index->manpage_begin, manindex->manpage_begin, manindex->manpage_len)) {
1223             continue;
1224         }
1225 
1226         tmp = QChar((manindex->manpage_begin)[0]).toLower();
1227         if (firstchar != tmp) {
1228             firstchar = tmp;
1229             os << "<tr><td class=\"secidxnextletter\""
1230                << " colspan=\"3\">\n  <a name=\"" << firstchar << "\">" << firstchar << "</a>\n</td></tr>\n";
1231 
1232             const QString appendixstr = QString(" [<a href=\"#%1\" accesskey=\"%2\">%3</a>]\n").arg(firstchar).arg(firstchar).arg(firstchar);
1233             indexLine.append(appendixstr);
1234         }
1235         os << "<tr><td><a href=\"man:" << manindex->manpath << "\">\n";
1236 
1237         ((char *)manindex->manpage_begin)[manindex->manpage_len] = '\0';
1238         os << manindex->manpage_begin << "</a></td><td>&nbsp;</td><td> "
1239            << (indexmap.contains(manindex->manpage_begin) ? indexmap[manindex->manpage_begin] : "") << "</td></tr>\n";
1240         last_index = manindex;
1241     }
1242     indexLine.append("</div>");
1243 
1244     for (int i = 0; i < listlen; i++) {
1245         ::free(indexlist[i]->manpath); // allocated by strdup
1246         delete indexlist[i];
1247     }
1248 
1249     delete[] indexlist;
1250 
1251     os << "</table></div>\n";
1252     os << "<br/><br/>\n";
1253 
1254     os << indexLine << '\n';
1255 
1256     // print footer
1257     outputFooter(os);
1258 
1259     // set the links "toolbar" also at the top
1260     os_h << indexLine << '\n';
1261     os_h.flush();
1262 
1263     infoMessage(QString());
1264     data(array_h + array_d);
1265     // Do not call finished(), the caller will do that
1266 }
1267 
1268 KIO::WorkerResult MANProtocol::listDir(const QUrl &url)
1269 {
1270     qCDebug(KIO_MAN_LOG) << url;
1271 
1272     QString title;
1273     QString section;
1274 
1275     if (!parseUrl(url.path(), title, section)) {
1276         return KIO::WorkerResult::fail(KIO::ERR_MALFORMED_URL, url.url());
1277     }
1278 
1279     // stat() and listDir() declared that everything is a HTML file.
1280     // However we can list man: and man:(1) as a directory (e.g. in dolphin).
1281     // But we cannot list man:ls as a directory, this makes no sense (#154173).
1282 
1283     if (!title.isEmpty() && title != "/") {
1284         return KIO::WorkerResult::fail(KIO::ERR_IS_FILE, url.url());
1285     }
1286 
1287     // There is no need to accumulate a list of UDSEntry's and deliver
1288     // them all in one block with WorkerBase::listEntries(), because
1289     // WorkerBase::listEntry() batches the entries and delivers them
1290     // timed to maximise application performance.
1291 
1292     UDSEntry uds_entry;
1293     uds_entry.reserve(4);
1294 
1295     uds_entry.fastInsert(KIO::UDSEntry::UDS_NAME, ".");
1296     uds_entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
1297     listEntry(uds_entry);
1298 
1299     if (section.isEmpty()) // list the top level directory
1300     {
1301         for (const QString &sect : m_sectionNames) {
1302             uds_entry.clear(); // sectionName() is already I18N'ed
1303             uds_entry.fastInsert(KIO::UDSEntry::UDS_NAME, (sect + " - " + sectionName(sect)));
1304             uds_entry.fastInsert(KIO::UDSEntry::UDS_URL, ("man:/(" + sect + ')'));
1305             uds_entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
1306             listEntry(uds_entry);
1307         }
1308     } else // list all pages in a section
1309     {
1310         const QStringList list = findPages(section, QString());
1311         for (const QString &page : list) {
1312             // Remove any compression suffix present
1313             QString name = stripCompression(page);
1314             QString displayName;
1315             // Remove any preceding pathname components, just leave the base name
1316             int pos = name.lastIndexOf('/');
1317             if (pos > 0)
1318                 name = name.mid(pos + 1);
1319             // Remove the section suffix
1320             pos = name.lastIndexOf('.');
1321             if (pos > 0) {
1322                 displayName = name.left(pos) + " (" + name.mid(pos + 1) + ')';
1323                 name.truncate(pos);
1324             }
1325 
1326             uds_entry.clear();
1327             uds_entry.fastInsert(KIO::UDSEntry::UDS_NAME, name);
1328             if (!displayName.isEmpty())
1329                 uds_entry.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, displayName);
1330             uds_entry.fastInsert(KIO::UDSEntry::UDS_URL, ("man:" + page));
1331             uds_entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFREG);
1332             uds_entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("text/html"));
1333             listEntry(uds_entry);
1334         }
1335     }
1336 
1337     return KIO::WorkerResult::pass();
1338 }
1339 
1340 bool MANProtocol::getProgramPath()
1341 {
1342     if (!mySgml2RoffPath.isEmpty())
1343         return true;
1344 
1345     mySgml2RoffPath = QStandardPaths::findExecutable(SGML2ROFF_EXECUTABLE);
1346     if (!mySgml2RoffPath.isEmpty())
1347         return true;
1348 
1349     /* sgml2roff isn't found in PATH. Check some possible locations where it may be found. */
1350     mySgml2RoffPath = QStandardPaths::findExecutable(SGML2ROFF_EXECUTABLE, QStringList(QLatin1String(SGML2ROFF_DIRS)));
1351     if (!mySgml2RoffPath.isEmpty())
1352         return true;
1353 
1354     /* Cannot find sgml2roff program: */
1355     outputError(xi18nc("@info",
1356                        "Could not find the <command>%1</command> program on your system. "
1357                        "Please install it if necessary, and ensure that it can be found using "
1358                        "the environment variable <envar>PATH</envar>.",
1359                        SGML2ROFF_EXECUTABLE));
1360     return false;
1361 }
1362 
1363 #include "kio_man.moc"
1364 #include "moc_kio_man.cpp"