/* This file is part of the KDE libraries SPDX-FileCopyrightText: 2000 Matthias Hoelzer-Kluepfel SPDX-License-Identifier: LGPL-2.0-or-later */ #include "kio_man.h" #include "kio_man_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include "man2html.h" #include using namespace KIO; // Pseudo plugin class to embed meta data class KIOPluginForMetaData : public QObject { Q_OBJECT Q_PLUGIN_METADATA(IID "org.kde.kio.worker.man" FILE "man.json") }; MANProtocol *MANProtocol::s_self = nullptr; static const char *SGML2ROFF_DIRS = "/usr/lib/sgml"; static const char *SGML2ROFF_EXECUTABLE = "sgml2roff"; /* * Drop trailing compression suffix from name */ static QString stripCompression(const QString &name) { int pos = name.length(); if (name.endsWith(".gz")) pos -= 3; else if (name.endsWith(".z", Qt::CaseInsensitive)) pos -= 2; else if (name.endsWith(".bz2")) pos -= 4; else if (name.endsWith(".bz")) pos -= 3; else if (name.endsWith(".lzma")) pos -= 5; else if (name.endsWith(".xz")) pos -= 3; else if (name.endsWith(".zst")) pos -= 4; else if (name.endsWith(".br")) pos -= 3; return (pos > 0 ? name.left(pos) : name); } /* * Drop trailing compression suffix and section from name */ static QString stripExtension(const QString &name) { QString wc = stripCompression(name); const int pos = wc.lastIndexOf('.'); return (pos > 0 ? wc.left(pos) : wc); } static bool parseUrl(const QString &_url, QString &title, QString §ion) { section.clear(); QString url = _url.trimmed(); if (url.isEmpty() || url.startsWith('/')) { if (url.isEmpty() || QFile::exists(url)) { // man:/usr/share/man/man1/ls.1.gz is a valid file title = url; return true; } else { // If a full path is specified but does not exist, // then it is perhaps a normal man page. qCDebug(KIO_MAN_LOG) << url << " does not exist"; } } while (url.startsWith('/')) url.remove(0, 1); title = url; int pos = url.indexOf('('); if (pos < 0) return true; // man:ls -> title=ls title = title.left(pos); section = url.mid(pos + 1); pos = section.indexOf(')'); if (pos >= 0) { if (pos < section.length() - 2 && title.isEmpty()) { title = section.mid(pos + 2); } section = section.left(pos); } // man:ls(2) -> title="ls", section="2" return true; } MANProtocol::MANProtocol(const QByteArray &pool_socket, const QByteArray &app_socket) : QObject() , WorkerBase("man", pool_socket, app_socket) { Q_ASSERT(s_self == nullptr); s_self = this; /* clang-format off */ m_sectionNames << "0" << "0p" << "1" << "1p" << "2" << "3" << "3n" << "3p" << "4" << "5" << "6" << "7" << "8" << "9" << "l" << "n"; /* clang-format on */ } MANProtocol *MANProtocol::self() { return s_self; } MANProtocol::~MANProtocol() { s_self = nullptr; } void MANProtocol::parseWhatIs(QMap &i, QTextStream &t, const QString &mark) { const QRegularExpression re(mark); QString l; while (!t.atEnd()) { l = t.readLine(); QRegularExpressionMatch match = re.match(l); int pos = match.capturedStart(0); if (pos != -1) { QString names = l.left(pos); QString descr = l.mid(match.capturedEnd(0)); while ((pos = names.indexOf(",")) != -1) { i[names.left(pos++)] = descr; while (names[pos] == ' ') pos++; names = names.mid(pos); } i[names] = descr; } } } bool MANProtocol::addWhatIs(QMap &i, const QString &name, const QString &mark) { QFile f(name); if (!f.open(QIODevice::ReadOnly)) return false; QTextStream t(&f); parseWhatIs(i, t, mark); return true; } QMap MANProtocol::buildIndexMap(const QString §ion) { qCDebug(KIO_MAN_LOG) << "for section" << section; QMap i; QStringList man_dirs = manDirectories(); // Supplementary places for whatis databases man_dirs += m_mandbpath; if (!man_dirs.contains("/var/cache/man")) man_dirs << "/var/cache/man"; if (!man_dirs.contains("/var/catman")) man_dirs << "/var/catman"; QStringList names; names << "whatis.db" << "whatis"; const QString mark = "\\s+\\(" + section + "[a-z]*\\)\\s+-\\s+"; int count0; for (const QString &it_dir : qAsConst(man_dirs)) { if (!QFile::exists(it_dir)) continue; bool added = false; for (const QString &it_name : qAsConst(names)) { count0 = i.count(); if (addWhatIs(i, (it_dir + '/' + it_name), mark)) { qCDebug(KIO_MAN_LOG) << "added" << (i.count() - count0) << "from" << it_name << "in" << it_dir; added = true; break; } } if (!added) { // Nothing was able to be added by scanning the directory, // so try parsing the output of the whatis(1) command. QProcess proc; proc.setProgram("whatis"); proc.setArguments(QStringList() << "-M" << it_dir << "-w" << "*"); proc.setProcessChannelMode(QProcess::ForwardedErrorChannel); proc.start(); proc.waitForFinished(); QTextStream t(proc.readAllStandardOutput(), QIODevice::ReadOnly); count0 = i.count(); parseWhatIs(i, t, mark); qCDebug(KIO_MAN_LOG) << "added" << (i.count() - count0) << "from whatis in" << it_dir; } } qCDebug(KIO_MAN_LOG) << "returning" << i.count() << "index entries"; return i; } //--------------------------------------------------------------------- QStringList MANProtocol::manDirectories() { checkManPaths(); // // Build a list of man directories including translations // QStringList man_dirs; const QList locales = QLocale::matchingLocales(QLocale::AnyLanguage, QLocale::AnyScript, QLocale::AnyCountry); for (const QString &it_dir : qAsConst(m_manpath)) { // Translated pages in "/" if the directory // exists for (const QLocale &it_loc : locales) { // TODO: languageToString() is wrong, that returns the readable name // of the language. We want the country code returned by name(). QString lang = QLocale::languageToString(it_loc.language()); if (!lang.isEmpty() && lang != QString("C")) { QString dir = it_dir + '/' + lang; QDir d(dir); if (d.exists()) { const QString p = d.canonicalPath(); if (!man_dirs.contains(p)) man_dirs += p; } } } // Untranslated pages in "" const QString p = QDir(it_dir).canonicalPath(); if (!man_dirs.contains(p)) man_dirs += p; } qCDebug(KIO_MAN_LOG) << "returning" << man_dirs.count() << "man directories"; return man_dirs; } QStringList MANProtocol::findPages(const QString &_section, const QString &title, bool full_path) { QStringList list; // qCDebug(KIO_MAN_LOG) << "findPages '" << section << "' '" << title << "'\n"; if (title.startsWith('/')) // absolute man page path { list.append(title); // just that and nothing else return list; } const QString star("*"); // flag for finding sections const QString man("man"); // prefix for ROFF subdirectories const QString sman("sman"); // prefix for SGML subdirectories // Generate a list of applicable sections QStringList sect_list; if (!_section.isEmpty()) // section requested as parameter { sect_list += _section; // It's not clear what this code does. If a section with a letter // suffix is specified, for example "3p", both "3p" and "3" are // added to the section list. The result is that "man:(3p)" returns // the same entries as "man:(3)" QString section = _section; while ((!section.isEmpty()) && (section.at(section.length() - 1).isLetter())) { section.truncate(section.length() - 1); sect_list += section; } } else { sect_list += star; } const QStringList man_dirs = manDirectories(); QStringList nameFilters; nameFilters += man + '*'; nameFilters += sman + '*'; // Find man pages in the sections specified above, // or all sections. // // Do not convert this loop to an iterator, or 'it_s' below // to a reference. The 'sect_list' list can be modified // within the loop. for (int i = 0; i < sect_list.count(); ++i) { const QString it_s = sect_list.at(i); QString it_real = it_s.toLower(); // Find applicable pages within all man directories. for (const QString &man_dir : man_dirs) { // Find all subdirectories named "man*" and "sman*" // to extend the list of sections and find the correct // case for the section suffix. QDir dp(man_dir); dp.setFilter(QDir::Dirs | QDir::NoDotAndDotDot); dp.setNameFilters(nameFilters); const QStringList entries = dp.entryList(); for (const QString &file : entries) { QString sect; if (file.startsWith(man)) sect = file.mid(man.length()); else if (file.startsWith(sman)) sect = file.mid(sman.length()); // Should never happen, because of the name filter above. if (sect.isEmpty()) continue; if (sect.toLower() == it_real) it_real = sect; // Only add sect if not already contained, avoid duplicates if (!sect_list.contains(sect) && _section.isEmpty()) { // qCDebug(KIO_MAN_LOG) << "another section " << sect; sect_list += sect; } } if (it_s != star) // finding pages, not just sections { const QString dir = man_dir + '/' + man + it_real + '/'; list.append(findManPagesInSection(dir, title, full_path)); const QString sdir = man_dir + '/' + sman + it_real + '/'; list.append(findManPagesInSection(sdir, title, full_path)); } } } // qCDebug(KIO_MAN_LOG) << "finished " << list << " " << sect_list; return list; } QStringList MANProtocol::findManPagesInSection(const QString &dir, const QString &title, bool full_path) { QStringList list; qCDebug(KIO_MAN_LOG) << "in" << dir << "title" << title; const bool title_given = !title.isEmpty(); QDir dp(dir); dp.setFilter(QDir::Files); const QStringList names = dp.entryList(); for (const QString &name : names) { if (title_given) { // check title if we're looking for a specific page if (!name.startsWith(title)) continue; // beginning matches, do a more thorough check... const QString tmp_name = stripExtension(name); if (tmp_name != title) continue; } list.append(full_path ? dir + name : name); } qCDebug(KIO_MAN_LOG) << "returning" << list.count() << "pages"; return (list); } //--------------------------------------------------------------------- void MANProtocol::output(const char *insert) { if (insert) { m_outputBuffer.write(insert, strlen(insert)); } if (!insert || m_outputBuffer.pos() >= 2048) { m_outputBuffer.close(); data(m_outputBuffer.buffer()); m_outputBuffer.setData(QByteArray()); m_outputBuffer.open(QIODevice::WriteOnly); } } #ifndef SIMPLE_MAN2HTML // called by man2html extern char *read_man_page(const char *filename) { return MANProtocol::self()->readManPage(filename); } // called by man2html extern void output_real(const char *insert) { MANProtocol::self()->output(insert); } #endif //--------------------------------------------------------------------- KIO::WorkerResult MANProtocol::get(const QUrl &url) { qCDebug(KIO_MAN_LOG) << "GET " << url.url(); // Indicate the mimetype - this will apply to both // formatted man pages and error pages. mimeType("text/html"); QString title, section; if (!parseUrl(url.path(), title, section)) { showMainIndex(); return KIO::WorkerResult::pass(); } // see if an index was requested if (url.query().isEmpty() && (title.isEmpty() || title == "/" || title == ".")) { if (section == "index" || section.isEmpty()) showMainIndex(); else showIndex(section); return KIO::WorkerResult::pass(); } QStringList foundPages = findPages(section, title); if (foundPages.isEmpty()) { outputError(xi18nc("@info", "No man page matching %1 could be found." "" "Check that you have not mistyped the name of the page, " "and note that man page names are case sensitive." "" "If the name is correct, then you may need to extend the search path " "for man pages, either using the MANPATH environment " "variable or a configuration file in the /etc " "directory.", title.toHtmlEscaped())); return KIO::WorkerResult::pass(); } // Sort the list of pages now, for display if required and for // testing for equivalents below. std::sort(foundPages.begin(), foundPages.end()); const QString pageFound = foundPages.first(); if (foundPages.count() > 1) { // See if the multiple pages found refer to the same man page, for example // if 'foo.1' and 'foo.1.gz' were both found. To make this generic with // regard to compression suffixes, assume that the first page name (after // the list has been sorted above) is the shortest. Then check that all of // the others are the same with a possible compression suffix added. for (int i = 1; i < foundPages.count(); ++i) { if (!foundPages[i].startsWith(pageFound + '.')) { // There is a page which is not the same as the reference, even // allowing for a compression suffix. Output the list of multiple // pages only. outputMatchingPages(foundPages); return KIO::WorkerResult::pass(); } } } m_outputBuffer.open(QIODevice::WriteOnly); const QByteArray filename = QFile::encodeName(pageFound); const char *buf = readManPage(filename); if (!buf) { // readManPage emits an error page instead, so still passing return KIO::WorkerResult::pass(); ; } // will call output_real scan_man_page(buf); delete[] buf; output(nullptr); // flush m_outputBuffer.close(); data(m_outputBuffer.buffer()); m_outputBuffer.setData(QByteArray()); // tell we are done data(QByteArray()); return KIO::WorkerResult::pass(); } //--------------------------------------------------------------------- // If this function returns nullptr to indicate a problem, // then it must call outputError() first. char *MANProtocol::readManPage(const char *_filename) { QByteArray filename = _filename; QByteArray array, dirName; // Determine the type of man page file by checking its path // Determination by MIME type with KMimeType doesn't work reliably. // E.g., Solaris 7: /usr/man/sman7fs/pcfs.7fs -> text/x-csrc - WRONG // // If the path name contains a component "sman", assume that it's SGML // and convert it to roff format (used on Solaris). Check for a pathname // component of "sman" only - we don't want a man page called "gasman.1" // to match. // QString file_mimetype = KMimeType::findByPath(QString(filename), 0, false)->name(); if (QString(filename).contains("/sman/", Qt::CaseInsensitive)) { QProcess proc; // Determine path to sgml2roff, if not already done. if (!getProgramPath()) return nullptr; proc.setProgram(mySgml2RoffPath); proc.setArguments(QStringList() << filename); proc.setProcessChannelMode(QProcess::ForwardedErrorChannel); proc.start(); proc.waitForFinished(); array = proc.readAllStandardOutput(); } else { if (QDir::isRelativePath(filename)) { qCDebug(KIO_MAN_LOG) << "relative" << filename; filename = QDir::cleanPath(lastdir + '/' + filename).toUtf8(); qCDebug(KIO_MAN_LOG) << "resolved to" << filename; } lastdir = filename.left(filename.lastIndexOf('/')); // get the last directory name (which might be a language name, to be able to guess the encoding) QDir dir(lastdir); dir.cdUp(); dirName = QFile::encodeName(dir.dirName()); if (!QFile::exists(QFile::decodeName(filename))) // if given file does not exist, find with suffix { qCDebug(KIO_MAN_LOG) << "not existing " << filename; QDir mandir(lastdir); const QString nameFilter = filename.mid(filename.lastIndexOf('/') + 1) + ".*"; mandir.setNameFilters(QStringList(nameFilter)); const QStringList entries = mandir.entryList(); if (entries.isEmpty()) { outputError(xi18nc("@info", "The specified man page references " "another page %1," "" "but the referenced page %2 " "could not be found.", QFile::decodeName(filename), QDir::cleanPath(lastdir + '/' + nameFilter))); return nullptr; } filename = lastdir + '/' + QFile::encodeName(entries.first()); qCDebug(KIO_MAN_LOG) << "resolved to " << filename; } KCompressionDevice fd(QFile::encodeName(filename)); if (!fd.open(QIODevice::ReadOnly)) { outputError(xi18nc("@info", "The man page %1 could not be read.", QFile::decodeName(filename))); return nullptr; } array = fd.readAll(); qCDebug(KIO_MAN_LOG) << "read " << array.size(); } if (array.isEmpty()) { outputError(xi18nc("@info", "The man page %1 could not be converted.", QFile::decodeName(filename))); return nullptr; } return manPageToUtf8(array, dirName); // never returns nullptr } //--------------------------------------------------------------------- // This opens one
to format the HTML body, so when the document // is finished it needs to be closed by outputFooter(). void MANProtocol::outputHeader(QTextStream &os, const QString &header, const QString &title) { const QString pageTitle = (!title.isEmpty() ? title : header); // The same header and styling as generated by man2html os << "\n"; os << "\n"; os << "" << pageTitle << "\n"; os << "\n"; os << "\n"; os << "\n"; os << "\n"; os << "\n"; os << "
\n"; os << "
\n"; os << "\"top-kde\"\n"; os << pageTitle << "\n"; os << "
\n"; os << "
\n"; os << "
\n"; os << "

" << header << "

\n"; os.flush(); } void MANProtocol::outputFooter(QTextStream &os) { os << "
\n"; // closes "
os << "
\n"; // closes "
os << "
\n"; // closes "
os << "\n"; os << "\n"; os.flush(); } // Do not call any other WorkerBase functions afterwards, // only return a KIO::WorkerResult::pass(). // It is assumed that mimeType() has already been called at the start of get(). void MANProtocol::outputError(const QString &errmsg) { QByteArray array; QTextStream os(&array, QIODevice::WriteOnly); outputHeader(os, i18n("Manual Page Viewer Error")); os << errmsg << "\n"; outputFooter(os); data(array); data(QByteArray()); } void MANProtocol::outputMatchingPages(const QStringList &matchingPages) { QByteArray array; QTextStream os(&array, QIODevice::WriteOnly); outputHeader(os, i18n("There is more than one matching man page:"), i18n("Multiple Manual Pages")); os << "
    \n"; int acckey = 1; for (const QString &page : matchingPages) { os << "
  • " << page << "
    \n
    \n"; ++acckey; } os << "
\n"; os << "
\n"; os << "

" << i18n( "Note: if you read a man page in your language," " be aware it can contain some mistakes or be obsolete." " In case of doubt, you should have a look at the English version.") << "

"; outputFooter(os); data(array); // Do not call finished(), the caller will do that } KIO::WorkerResult MANProtocol::stat(const QUrl &url) { qCDebug(KIO_MAN_LOG) << "STAT " << url.url(); QString title, section; if (!parseUrl(url.path(), title, section)) { return KIO::WorkerResult::fail(KIO::ERR_MALFORMED_URL, url.url()); } qCDebug(KIO_MAN_LOG) << "URL" << url.url() << "parsed to title" << title << "section" << section; UDSEntry entry; entry.reserve(3); entry.fastInsert(KIO::UDSEntry::UDS_NAME, title); entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFREG); entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("text/html")); statEntry(entry); return KIO::WorkerResult::pass(); } extern "C" { int Q_DECL_EXPORT kdemain(int argc, char **argv) { QCoreApplication app(argc, argv); app.setApplicationName(QLatin1String("kio_man")); qCDebug(KIO_MAN_LOG) << "STARTING"; if (argc != 4) { fprintf(stderr, "Usage: kio_man protocol domain-socket1 domain-socket2\n"); exit(-1); } MANProtocol worker(argv[2], argv[3]); worker.dispatchLoop(); qCDebug(KIO_MAN_LOG) << "Done"; return 0; } } KIO::WorkerResult MANProtocol::mimetype(const QUrl & /*url*/) { mimeType("text/html"); return KIO::WorkerResult::pass(); } //--------------------------------------------------------------------- static QString sectionName(const QString §ion) { if (section == "0") return i18n("Header Files"); else if (section == "0p") return i18n("Header Files (POSIX)"); else if (section == "1") return i18n("User Commands"); else if (section == "1p") return i18n("User Commands (POSIX)"); else if (section == "2") return i18n("System Calls"); else if (section == "3") return i18n("Subroutines"); // TODO: current usage of '3p' seems to be "Subroutines (POSIX)" else if (section == "3p") return i18n("Perl Modules"); else if (section == "3n") return i18n("Network Functions"); else if (section == "4") return i18n("Devices"); else if (section == "5") return i18n("File Formats"); else if (section == "6") return i18n("Games"); else if (section == "7") return i18n("Miscellaneous"); else if (section == "8") return i18n("System Administration"); else if (section == "9") return i18n("Kernel"); else if (section == "l") return i18n("Local Documentation"); // TODO: current usage of 'n' seems to be TCL commands else if (section == "n") return i18n("New"); return QString(); } QStringList MANProtocol::buildSectionList(const QStringList &dirs) const { QStringList l; for (const QString &it_sect : qAsConst(m_sectionNames)) { for (const QString &it_dir : dirs) { QDir d(it_dir + "/man" + it_sect); if (d.exists()) { l << it_sect; break; } } } return l; } //--------------------------------------------------------------------- void MANProtocol::showMainIndex() { QByteArray array; QTextStream os(&array, QIODevice::WriteOnly); outputHeader(os, i18n("Main Manual Page Index")); // ### TODO: why still the environment variable // if keeping it, also use it in listDir() const QString sectList = qgetenv("MANSECT"); QStringList sections; if (sectList.isEmpty()) sections = buildSectionList(manDirectories()); else sections = sectList.split(':'); os << "\n"; QSet accessKeys; char alternateAccessKey = 'a'; for (const QString &it_sect : qAsConst(sections)) { if (it_sect.isEmpty()) continue; // guard back() below // create a unique access key QChar accessKey = it_sect.back(); // rightmost char while (accessKeys.contains(accessKey)) accessKey = alternateAccessKey++; accessKeys.insert(accessKey); os << "\n"; } os << "
" << i18n("Section %1", it_sect) << "  " << sectionName(it_sect) << "
\n"; outputFooter(os); data(array); data(QByteArray()); // Do not call finished(), the caller will do that } //--------------------------------------------------------------------- // TODO: constr_catmanpath modified here, should parameter be passed by reference? void MANProtocol::constructPath(QStringList &constr_path, QStringList constr_catmanpath) { QMap manpath_map; QMap mandb_map; // Add paths from /etc/man.conf // // Explicit manpaths may be given by lines starting with "MANPATH" or // "MANDATORY_MANPATH" (depending on system ?). // Mappings from $PATH to manpath are given by lines starting with // "MANPATH_MAP" // The entry is e.g. "MANDATORY_MANPATH " const QRegularExpression manpath_regex("^(?:MANPATH|MANDATORY_MANPATH)\\s+(\\S+)"); // The entry is "MANPATH_MAP " const QRegularExpression manpath_map_regex("^MANPATH_MAP\\s+(\\S+)\\s+(\\S+)"); // The entry is "MANDB_MAP " const QRegularExpression mandb_map_regex("^MANDB_MAP\\s+(\\S+)\\s+(\\S+)"); QFile mc("/etc/man.conf"); // Caldera if (!mc.exists()) mc.setFileName("/etc/manpath.config"); // SuSE, Debian if (!mc.exists()) mc.setFileName("/etc/man.config"); // Mandrake if (mc.open(QIODevice::ReadOnly)) { QTextStream is(&mc); while (!is.atEnd()) { const QString line = is.readLine(); QRegularExpressionMatch rmatch; if (line.contains(manpath_regex, &rmatch)) { constr_path += rmatch.captured(1); } else if (line.contains(manpath_map_regex, &rmatch)) { const QString dir = QDir::cleanPath(rmatch.captured(1)); const QString mandir = QDir::cleanPath(rmatch.captured(2)); manpath_map[dir] = mandir; } else if (line.contains(mandb_map_regex, &rmatch)) { const QString mandir = QDir::cleanPath(rmatch.captured(1)); const QString catmandir = QDir::cleanPath(rmatch.captured(2)); mandb_map[mandir] = catmandir; } /* sections are not used else if ( section_regex.find(line, 0) == 0 ) { if ( !conf_section.isEmpty() ) conf_section += ':'; conf_section += line.mid(8).trimmed(); } */ } mc.close(); } // Default paths static const char *const manpaths[] = {"/usr/X11/man", "/usr/X11R6/man", "/usr/man", "/usr/local/man", "/usr/exp/man", "/usr/openwin/man", "/usr/dt/man", "/opt/freetool/man", "/opt/local/man", "/usr/tex/man", "/usr/www/man", "/usr/lang/man", "/usr/gnu/man", "/usr/share/man", "/usr/motif/man", "/usr/titools/man", "/usr/sunpc/man", "/usr/ncd/man", "/usr/newsprint/man", nullptr}; int i = 0; while (manpaths[i]) { if (constr_path.indexOf(QString(manpaths[i])) == -1) constr_path += QString(manpaths[i]); i++; } // Directories in $PATH // - if a manpath mapping exists, use that mapping // - if a directory "/man" or "/../man" exists, add it // to the man path (the actual existence check is done further down) if (::getenv("PATH")) { const QStringList path = QString::fromLocal8Bit(::getenv("PATH")).split(':', Qt::SkipEmptyParts); for (const QString &it : qAsConst(path)) { const QString dir = QDir::cleanPath(it); QString mandir = manpath_map[dir]; if (!mandir.isEmpty()) { // a path mapping exists if (constr_path.indexOf(mandir) == -1) constr_path += mandir; } else { // no manpath mapping, use "/man" and "/../man" mandir = dir + QString("/man"); if (constr_path.indexOf(mandir) == -1) constr_path += mandir; int pos = dir.lastIndexOf('/'); if (pos > 0) { mandir = dir.left(pos) + QString("/man"); if (constr_path.indexOf(mandir) == -1) constr_path += mandir; } } QString catmandir = mandb_map[mandir]; if (!mandir.isEmpty()) { if (constr_catmanpath.indexOf(catmandir) == -1) constr_catmanpath += catmandir; } else { // What is the default mapping? catmandir = mandir; catmandir.replace("/usr/share/", "/var/cache/"); if (constr_catmanpath.indexOf(catmandir) == -1) constr_catmanpath += catmandir; } } } } void MANProtocol::checkManPaths() { static bool inited = false; if (inited) return; inited = true; const QString manpath_env = qgetenv("MANPATH"); // Decide if $MANPATH is enough on its own or if it should be merged // with the constructed path. // A $MANPATH starting or ending with ":", or containing "::", // should be merged with the constructed path. const bool construct_path = (manpath_env.isEmpty() || manpath_env.startsWith(':') || manpath_env.endsWith(':') || manpath_env.contains("::")); // Constructed man path -- consists of paths from // /etc/man.conf // default dirs // $PATH QStringList constr_path; QStringList constr_catmanpath; // catmanpath if (construct_path) // need to read config file { constructPath(constr_path, constr_catmanpath); } m_mandbpath = constr_catmanpath; // Merge $MANPATH with the constructed path to form the // actual manpath. // // The merging syntax with ":" and "::" in $MANPATH will be // satisfied if any empty string in path_list_env (there // should be 1 or 0) is replaced by the constructed path. const QStringList path_list_env = manpath_env.split(':', Qt::KeepEmptyParts); for (const QString &dir : path_list_env) { if (!dir.isEmpty()) // non empty part - add if exists { if (m_manpath.contains(dir)) continue; // ignore if already present if (QDir(dir).exists()) m_manpath += dir; // add to list if exists } else // empty part - merge constructed path { // Insert constructed path ($MANPATH was empty, or // there was a ":" at either end or "::" within) for (const QString &dir2 : qAsConst(constr_path)) { if (dir2.isEmpty()) continue; // don't add a null entry if (m_manpath.contains(dir2)) continue; // ignore if already present // add to list if exists if (QDir(dir2).exists()) m_manpath += dir2; } } } /* sections are not used // Sections QStringList m_mansect = mansect_env.split( ':', Qt::KeepEmptyParts); const char* const default_sect[] = { "1", "2", "3", "4", "5", "6", "7", "8", "9", "n", 0L }; for ( int i = 0; default_sect[i] != 0L; i++ ) if ( m_mansect.indexOf( QString( default_sect[i] ) ) == -1 ) m_mansect += QString( default_sect[i] ); */ qCDebug(KIO_MAN_LOG) << "manpath" << m_manpath; } // Setup my own structure, with char pointers. // from now on only pointers are copied, no strings // // containing the whole path string, // the beginning of the man page name // and the length of the name struct man_index_t { char *manpath; // the full path including man file const char *manpage_begin; // pointer to the begin of the man file name in the path int manpage_len; // len of the man file name }; typedef man_index_t *man_index_ptr; int compare_man_index(const void *s1, const void *s2) { struct man_index_t *m1 = *(struct man_index_t **)s1; struct man_index_t *m2 = *(struct man_index_t **)s2; int i; // Compare the names of the pages // with the shorter length. // Man page names are not '\0' terminated, so // this is a bit tricky if (m1->manpage_len > m2->manpage_len) { i = qstrnicmp(m1->manpage_begin, m2->manpage_begin, m2->manpage_len); if (!i) return 1; return i; } if (m1->manpage_len < m2->manpage_len) { i = qstrnicmp(m1->manpage_begin, m2->manpage_begin, m1->manpage_len); if (!i) return -1; return i; } return qstrnicmp(m1->manpage_begin, m2->manpage_begin, m1->manpage_len); } void MANProtocol::showIndex(const QString §ion) { QByteArray array_h; QTextStream os_h(&array_h, QIODevice::WriteOnly); // print header outputHeader(os_h, i18n("Index for section %1: %2", section, sectionName(section)), i18n("Manual Page Index")); QByteArray array_d; QTextStream os(&array_d, QIODevice::WriteOnly); os << "
\n"; // compose list of search paths ------------------------------------------------------------- checkManPaths(); infoMessage(i18n("Generating Index")); // search for the man pages QStringList pages = findPages(section, QString()); if (pages.isEmpty()) // not a single page found { // print footer outputFooter(os); os_h.flush(); infoMessage(QString()); data(array_h + array_d); return; } QMap indexmap = buildIndexMap(section); // print out the list os << "

\n"; os << "\n"; int listlen = pages.count(); man_index_ptr *indexlist = new man_index_ptr[listlen]; listlen = 0; QStringList::const_iterator page; for (const QString &page : qAsConst(pages)) { // I look for the beginning of the man page name // i.e. "bla/pagename.3.gz" by looking for the last "/" // Then look for the end of the name by searching backwards // for the last ".", not counting zip extensions. // If the len of the name is >0, // store it in the list structure, to be sorted later struct man_index_t *manindex = new man_index_t; manindex->manpath = strdup(page.toUtf8()); manindex->manpage_begin = strrchr(manindex->manpath, '/'); if (manindex->manpage_begin) { manindex->manpage_begin++; Q_ASSERT(manindex->manpage_begin >= manindex->manpath); } else { manindex->manpage_begin = manindex->manpath; Q_ASSERT(manindex->manpage_begin >= manindex->manpath); } // Skip extension ".section[.gz]" const char *begin = manindex->manpage_begin; const int len = strlen(begin); const char *end = begin + (len - 1); if (len >= 3 && strcmp(end - 2, ".gz") == 0) end -= 3; else if (len >= 2 && strcmp(end - 1, ".Z") == 0) end -= 2; else if (len >= 2 && strcmp(end - 1, ".z") == 0) end -= 2; else if (len >= 4 && strcmp(end - 3, ".bz2") == 0) end -= 4; else if (len >= 5 && strcmp(end - 4, ".lzma") == 0) end -= 5; else if (len >= 3 && strcmp(end - 2, ".xz") == 0) end -= 3; while (end >= begin && *end != '.') end--; if (end < begin) { // no '.' ending ??? // set the pointer past the end of the filename manindex->manpage_len = page.length(); manindex->manpage_len -= (manindex->manpage_begin - manindex->manpath); Q_ASSERT(manindex->manpage_len >= 0); } else { const char *manpage_end = end; manindex->manpage_len = (manpage_end - manindex->manpage_begin); Q_ASSERT(manindex->manpage_len >= 0); } if (manindex->manpage_len > 0) { indexlist[listlen] = manindex; listlen++; } else delete manindex; } // // Now do the sorting on the page names // and the printout afterwards // While printing avoid duplicate man page names // struct man_index_t dummy_index = {nullptr, nullptr, 0}; struct man_index_t *last_index = &dummy_index; // sort and print qsort(indexlist, listlen, sizeof(struct man_index_t *), compare_man_index); QChar firstchar, tmp; QString indexLine = "
\n"; if (indexlist[0]->manpage_len > 0) { firstchar = QChar((indexlist[0]->manpage_begin)[0]).toLower(); const QString appendixstr = QString(" [%3]\n").arg(firstchar).arg(firstchar).arg(firstchar); indexLine.append(appendixstr); } os << "
\n"; for (int i = 0; i < listlen; i++) { struct man_index_t *manindex = indexlist[i]; // qstrncmp(): // "last_man" has already a \0 string ending, but // "manindex->manpage_begin" has not, // so do compare at most "manindex->manpage_len" of the strings. if (last_index->manpage_len == manindex->manpage_len && !qstrncmp(last_index->manpage_begin, manindex->manpage_begin, manindex->manpage_len)) { continue; } tmp = QChar((manindex->manpage_begin)[0]).toLower(); if (firstchar != tmp) { firstchar = tmp; os << "\n"; const QString appendixstr = QString(" [%3]\n").arg(firstchar).arg(firstchar).arg(firstchar); indexLine.append(appendixstr); } os << "\n"; last_index = manindex; } indexLine.append(""); for (int i = 0; i < listlen; i++) { ::free(indexlist[i]->manpath); // allocated by strdup delete indexlist[i]; } delete[] indexlist; os << "
\n"; os << "" << firstchar << "\n"; os << "
\n " << firstchar << "\n
manpath << "\">\n"; ((char *)manindex->manpage_begin)[manindex->manpage_len] = '\0'; os << manindex->manpage_begin << "  " << (indexmap.contains(manindex->manpage_begin) ? indexmap[manindex->manpage_begin] : "") << "
\n"; os << "

\n"; os << indexLine << '\n'; // print footer outputFooter(os); // set the links "toolbar" also at the top os_h << indexLine << '\n'; os_h.flush(); infoMessage(QString()); data(array_h + array_d); // Do not call finished(), the caller will do that } KIO::WorkerResult MANProtocol::listDir(const QUrl &url) { qCDebug(KIO_MAN_LOG) << url; QString title; QString section; if (!parseUrl(url.path(), title, section)) { return KIO::WorkerResult::fail(KIO::ERR_MALFORMED_URL, url.url()); } // stat() and listDir() declared that everything is a HTML file. // However we can list man: and man:(1) as a directory (e.g. in dolphin). // But we cannot list man:ls as a directory, this makes no sense (#154173). if (!title.isEmpty() && title != "/") { return KIO::WorkerResult::fail(KIO::ERR_IS_FILE, url.url()); } // There is no need to accumulate a list of UDSEntry's and deliver // them all in one block with WorkerBase::listEntries(), because // WorkerBase::listEntry() batches the entries and delivers them // timed to maximise application performance. UDSEntry uds_entry; uds_entry.reserve(4); uds_entry.fastInsert(KIO::UDSEntry::UDS_NAME, "."); uds_entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); listEntry(uds_entry); if (section.isEmpty()) // list the top level directory { for (const QString § : m_sectionNames) { uds_entry.clear(); // sectionName() is already I18N'ed uds_entry.fastInsert(KIO::UDSEntry::UDS_NAME, (sect + " - " + sectionName(sect))); uds_entry.fastInsert(KIO::UDSEntry::UDS_URL, ("man:/(" + sect + ')')); uds_entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); listEntry(uds_entry); } } else // list all pages in a section { const QStringList list = findPages(section, QString()); for (const QString &page : list) { // Remove any compression suffix present QString name = stripCompression(page); QString displayName; // Remove any preceding pathname components, just leave the base name int pos = name.lastIndexOf('/'); if (pos > 0) name = name.mid(pos + 1); // Remove the section suffix pos = name.lastIndexOf('.'); if (pos > 0) { displayName = name.left(pos) + " (" + name.mid(pos + 1) + ')'; name.truncate(pos); } uds_entry.clear(); uds_entry.fastInsert(KIO::UDSEntry::UDS_NAME, name); if (!displayName.isEmpty()) uds_entry.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, displayName); uds_entry.fastInsert(KIO::UDSEntry::UDS_URL, ("man:" + page)); uds_entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFREG); uds_entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("text/html")); listEntry(uds_entry); } } return KIO::WorkerResult::pass(); } bool MANProtocol::getProgramPath() { if (!mySgml2RoffPath.isEmpty()) return true; mySgml2RoffPath = QStandardPaths::findExecutable(SGML2ROFF_EXECUTABLE); if (!mySgml2RoffPath.isEmpty()) return true; /* sgml2roff isn't found in PATH. Check some possible locations where it may be found. */ mySgml2RoffPath = QStandardPaths::findExecutable(SGML2ROFF_EXECUTABLE, QStringList(QLatin1String(SGML2ROFF_DIRS))); if (!mySgml2RoffPath.isEmpty()) return true; /* Cannot find sgml2roff program: */ outputError(xi18nc("@info", "Could not find the %1 program on your system. " "Please install it if necessary, and ensure that it can be found using " "the environment variable PATH.", SGML2ROFF_EXECUTABLE)); return false; } #include "kio_man.moc" #include "moc_kio_man.cpp"