Files
wxWidgets/src/html/chm.cpp
Vadim Zeitlin f37d449208 Add convenient wxFileName::GetAbsolutePath() wrapper and use it
This wrapper simply combines the calls to MakeAbsolute() and
GetFullPath(), but using it results in shorter and more clear code, so
it seems to be worth having.
2021-07-11 14:51:31 +01:00

916 lines
24 KiB
C++

/////////////////////////////////////////////////////////////////////////////
// Name: src/html/chm.cpp
// Purpose: CHM (Help) support for wxHTML
// Author: Markus Sinner
// Copyright: (c) 2003 Herd Software Development
// Licence: wxWindows licence
/////////////////////////////////////////////////////////////////////////////
#include "wx/wxprec.h"
#if wxUSE_LIBMSPACK
#include <mspack.h>
#ifndef WX_PRECOMP
#include "wx/intl.h"
#include "wx/log.h"
#include "wx/module.h"
#endif
#include "wx/filesys.h"
#include "wx/mstream.h"
#include "wx/wfstream.h"
#include "wx/html/forcelnk.h"
FORCE_LINK_ME(wxhtml_chm_support)
// ----------------------------------------------------------------------------
/// wxChmTools
/// <p>
/// this class is used to abstract access to CHM-Archives
/// with library mspack written by Stuart Caie
/// http://www.kyz.uklinux.net/libmspack/
// ----------------------------------------------------------------------------
class wxChmTools
{
public:
/// constructor
wxChmTools(const wxFileName &archive);
/// destructor
~wxChmTools();
/// Generate error-string for error-code
static const wxString ChmErrorMsg(int error);
/// get an array of archive-member-filenames
const wxArrayString *GetFileNames()
{
return m_fileNames;
}
/// get the name of the archive represented by this class
const wxString GetArchiveName() const
{
return m_chmFileName;
}
/// Find a file in the archive
const wxString Find(const wxString& pattern,
const wxString& startfrom = wxEmptyString);
/// Extract a file in the archive into a file
size_t Extract(const wxString& pattern, const wxString& filename);
/// check archive for a file
bool Contains(const wxString& pattern);
/// get a string for the last error which occurred
const wxString GetLastErrorMessage();
/// Last Error
int m_lasterror;
private:
// these vars are used by FindFirst/Next:
wxString m_chmFileName;
char *m_chmFileNameANSI;
/// mspack-pointer to mschmd_header
struct mschmd_header *m_archive;
/// mspack-pointer to mschm_decompressor
struct mschm_decompressor *m_decompressor;
/// Array of filenames in archive
wxArrayString * m_fileNames;
/// Internal function to get filepointer
struct mschmd_file *GetMschmdFile(const wxString& pattern);
};
/***
* constructor
*
* @param archive The filename of the archive to open
*/
wxChmTools::wxChmTools(const wxFileName &archive)
{
m_chmFileName = archive.GetFullPath();
wxASSERT_MSG( !m_chmFileName.empty(), wxT("empty archive name") );
m_archive = NULL;
m_decompressor = NULL;
m_fileNames = NULL;
m_lasterror = 0;
struct mschmd_header *chmh;
struct mschm_decompressor *chmd;
struct mschmd_file *file;
// Create decompressor
chmd = mspack_create_chm_decompressor(NULL);
m_decompressor = (struct mschm_decompressor *) chmd;
// NB: we must make a copy of the string because chmd->open won't call
// strdup() [libmspack-20030726], which would cause crashes in
// Unicode build when mb_str() returns temporary buffer
m_chmFileNameANSI = strdup((const char*)m_chmFileName.mb_str(wxConvFile));
// Open the archive and store it in class:
if ( (chmh = chmd->open(chmd, (char*)m_chmFileNameANSI)) )
{
m_archive = chmh;
// Create Filenamearray
m_fileNames = new wxArrayString;
// Store Filenames in array
for (file = chmh->files; file; file = file->next)
{
m_fileNames->Add(wxString::FromAscii(file->filename));
}
}
else
{
wxLogError(_("Failed to open CHM archive '%s'."),
archive.GetFullPath());
m_lasterror = (chmd->last_error(chmd));
return;
}
}
/***
* Destructor
*/
wxChmTools::~wxChmTools()
{
struct mschm_decompressor *chmd = m_decompressor;
struct mschmd_header *chmh = m_archive;
delete m_fileNames;
// Close Archive
if (chmh && chmd)
chmd->close(chmd, chmh);
free(m_chmFileNameANSI);
// Destroy Decompressor
if (chmd)
mspack_destroy_chm_decompressor(chmd);
}
/**
* Checks if the given pattern matches to any
* filename stored in archive
*
* @param pattern The filename pattern, may include '*' and/or '?'
* @return true, if any file matching pattern has been found,
* false if not
*/
bool wxChmTools::Contains(const wxString& pattern)
{
int count;
wxString pattern_tmp = wxString(pattern).MakeLower();
// loop through filearay
if ( m_fileNames && (count = m_fileNames->GetCount()) > 0 )
{
for (int i = 0; i < count; i++)
{
wxString tmp = m_fileNames->Item(i).MakeLower();
if ( tmp.Matches(pattern_tmp) || tmp.Mid(1).Matches(pattern_tmp))
return true;
}
}
return false;
}
/**
* Find()
*
* Finds the next file described by a pattern in the archive, starting
* the file given by second parameter
*
* @param pattern The file-pattern to search for. May contain '*' and/or '?'
* @param startfrom The filename which the search should start after
* @returns The full pathname of the found file
*/
const wxString wxChmTools::Find(const wxString& pattern,
const wxString& startfrom)
{
int count;
wxString tmp;
wxString pattern_tmp(pattern);
wxString startfrom_tmp(startfrom);
pattern_tmp.MakeLower();
startfrom_tmp.MakeLower();
if ( m_fileNames && (count = m_fileNames->GetCount()) > 0 )
{
for (int i = 0; i < count; i++)
{
tmp = m_fileNames->Item(i).MakeLower();
// if we find the string where the search should began
if ( tmp.Matches(startfrom_tmp) ||
tmp.Mid(1).Matches(startfrom_tmp) )
continue;
if ( tmp.Matches(pattern_tmp) ||
tmp.Mid(1).Matches(pattern_tmp) )
{
return tmp;
}
}
}
return wxEmptyString;
}
/**
* Extract ()
*
* extracts the first hit of pattern to the given position
*
* @param pattern A filename pattern (may contain * and ? chars)
* @param filename The FileName where to temporary extract the file to
* @return 0 at no file extracted<br>
* number of bytes extracted else
*/
size_t wxChmTools::Extract(const wxString& pattern, const wxString& filename)
{
struct mschm_decompressor *d = m_decompressor;
struct mschmd_header *h = m_archive;
struct mschmd_file *f;
wxString tmp;
wxString pattern_tmp = (wxString(pattern)).MakeLower();
for (f = h->files; f; f = f->next)
{
tmp = wxString::FromAscii(f->filename).MakeLower();
if ( tmp.Matches(pattern_tmp) ||
tmp.Mid(1).Matches(pattern_tmp) )
{
// ignore leading '/'
if (d->extract(d, f,
(char*)(const char*)filename.mb_str(wxConvFile)))
{
// Error
m_lasterror = d->last_error(d);
wxLogError(_("Could not extract %s into %s: %s"),
wxString::FromAscii(f->filename),
filename,
ChmErrorMsg(m_lasterror));
return 0;
}
else
{
return (size_t) f->length;
}
}
}
return 0;
}
/**
* Find a file by pattern
*
* @param pattern A filename pattern (may contain * and ? chars)
* @return A pointer to the file (mschmd_file*)
*/
struct mschmd_file *wxChmTools::GetMschmdFile(const wxString& pattern_orig)
{
struct mschmd_file *f;
struct mschmd_header *h = (struct mschmd_header *) m_archive;
wxString tmp;
wxString pattern = wxString(pattern_orig).MakeLower();
for (f = h->files; f; f = f->next)
{
tmp = wxString::FromAscii(f->filename).MakeLower();
if ( tmp.Matches(pattern) || tmp.Mid(1).Matches(pattern) )
{
// ignore leading '/'
return f;
}
}
return NULL;
}
const wxString wxChmTools::GetLastErrorMessage()
{
return ChmErrorMsg(m_lasterror);
}
const wxString wxChmTools::ChmErrorMsg(int error)
{
switch (error)
{
case MSPACK_ERR_OK:
return _("no error");
case MSPACK_ERR_ARGS:
return _("bad arguments to library function");
case MSPACK_ERR_OPEN:
return _("error opening file");
case MSPACK_ERR_READ:
return _("read error");
case MSPACK_ERR_WRITE:
return _("write error");
case MSPACK_ERR_SEEK:
return _("seek error");
case MSPACK_ERR_NOMEMORY:
return _("out of memory");
case MSPACK_ERR_SIGNATURE:
return _("bad signature");
case MSPACK_ERR_DATAFORMAT:
return _("error in data format");
case MSPACK_ERR_CHECKSUM:
return _("checksum error");
case MSPACK_ERR_CRUNCH:
return _("compression error");
case MSPACK_ERR_DECRUNCH:
return _("decompression error");
}
return _("unknown error");
}
// ---------------------------------------------------------------------------
/// wxChmInputStream
// ---------------------------------------------------------------------------
class wxChmInputStream : public wxInputStream
{
public:
/// Constructor
wxChmInputStream(const wxString& archive,
const wxString& file, bool simulate = false);
/// Destructor
virtual ~wxChmInputStream();
/// Return the size of the accessed file in archive
virtual size_t GetSize() const wxOVERRIDE { return m_size; }
/// End of Stream?
virtual bool Eof() const wxOVERRIDE;
/// Set simulation-mode of HHP-File (if non is found)
void SimulateHHP(bool sim) { m_simulateHHP = sim; }
protected:
/// See wxInputStream
virtual size_t OnSysRead(void *buffer, size_t bufsize) wxOVERRIDE;
/// See wxInputStream
virtual wxFileOffset OnSysSeek(wxFileOffset seek, wxSeekMode mode) wxOVERRIDE;
/// See wxInputStream
virtual wxFileOffset OnSysTell() const wxOVERRIDE { return m_pos; }
private:
size_t m_size;
wxFileOffset m_pos;
bool m_simulateHHP;
char * m_content;
wxInputStream * m_contentStream;
void CreateHHPStream();
bool CreateFileStream(const wxString& pattern);
// this void* is handle of archive . I'm sorry it is void and not proper
// type but I don't want to make unzip.h header public.
// should store pointer to current file
mspack_file *m_file;
// The Chm-Class for extracting the data
wxChmTools *m_chm;
wxString m_fileName;
};
/**
* Constructor
* @param archive The name of the .chm archive. Remember that archive must
* be local file accessible via fopen, fread functions!
* @param filename The Name of the file to be extracted from archive
* @param simulate if true than class should simulate .HHP-File based on #SYSTEM
* if false than class does nothing if it doesn't find .hhp
*/
wxChmInputStream::wxChmInputStream(const wxString& archive,
const wxString& filename, bool simulate)
: wxInputStream()
{
m_pos = 0;
m_size = 0;
m_content = NULL;
m_contentStream = NULL;
m_lasterror = wxSTREAM_NO_ERROR;
m_chm = new wxChmTools (wxFileName(archive));
m_file = NULL;
m_fileName = wxString(filename).MakeLower();
m_simulateHHP = simulate;
if ( !m_chm->Contains(m_fileName) )
{
// if the file could not be located, but was *.hhp, than we create
// the content of the hhp-file on the fly and store it for reading
// by the application
if ( m_fileName.Find(wxT(".hhp")) != wxNOT_FOUND && m_simulateHHP )
{
// now we open an hhp-file
CreateHHPStream();
}
else
{
wxLogError(_("Could not locate file '%s'."), filename);
m_lasterror = wxSTREAM_READ_ERROR;
return;
}
}
else
{ // file found
CreateFileStream(m_fileName);
}
}
wxChmInputStream::~wxChmInputStream()
{
delete m_chm;
delete m_contentStream;
if (m_content)
{
free (m_content);
m_content=NULL;
}
}
bool wxChmInputStream::Eof() const
{
return (m_content==NULL ||
m_contentStream==NULL ||
m_contentStream->Eof() ||
(size_t)m_pos>m_size);
}
size_t wxChmInputStream::OnSysRead(void *buffer, size_t bufsize)
{
if ( (size_t)m_pos >= m_size )
{
m_lasterror = wxSTREAM_EOF;
return 0;
}
m_lasterror = wxSTREAM_NO_ERROR;
// If the rest to read from the stream is less
// than the buffer size, then only read the rest
if ( m_pos + bufsize > m_size )
bufsize = m_size - m_pos;
if (m_contentStream->SeekI(m_pos) == wxInvalidOffset)
{
m_lasterror = wxSTREAM_EOF;
return 0;
}
size_t read = m_contentStream->Read(buffer, bufsize).LastRead();
m_pos += read;
if (m_contentStream->SeekI(m_pos) == wxInvalidOffset)
{
m_lasterror = wxSTREAM_READ_ERROR;
return 0;
}
if (read != bufsize)
m_lasterror = m_contentStream->GetLastError();
return read;
}
wxFileOffset wxChmInputStream::OnSysSeek(wxFileOffset seek, wxSeekMode mode)
{
wxString mode_str;
if ( !m_contentStream || m_contentStream->Eof() )
{
m_lasterror = wxSTREAM_EOF;
return 0;
}
m_lasterror = wxSTREAM_NO_ERROR;
wxFileOffset nextpos;
switch ( mode )
{
case wxFromCurrent:
nextpos = seek + m_pos;
break;
case wxFromStart:
nextpos = seek;
break;
case wxFromEnd:
nextpos = m_size - 1 + seek;
break;
default:
nextpos = m_pos;
break; /* just to fool compiler, never happens */
}
m_pos=nextpos;
// Set current position on stream
m_contentStream->SeekI(m_pos);
return m_pos;
}
/**
* Help Browser tries to read the contents of the
* file by interpreting a .hhp file in the Archive.
* Because .chm doesn't include such a file, we need
* to rebuild the information based on stored
* system-files.
*/
void
wxChmInputStream::CreateHHPStream()
{
wxFileName file;
bool hhc = false;
bool hhk = false;
wxInputStream *i;
wxMemoryOutputStream *out;
const char *tmp;
// Try to open the #SYSTEM-File and create the HHP File out of it
// see http://bonedaddy.net/pabs3/chmspec/0.1.2/Internal.html#SYSTEM
if ( ! m_chm->Contains(wxT("/#SYSTEM")) )
{
#ifdef DEBUG
wxLogDebug("Archive doesn't contain #SYSTEM file");
#endif
return;
}
else
{
file = wxFileName(wxT("/#SYSTEM"));
}
if ( CreateFileStream(wxT("/#SYSTEM")) )
{
// New stream for writing a memory area to simulate the
// .hhp-file
out = new wxMemoryOutputStream();
tmp = "[OPTIONS]\r\n";
out->Write((const void *) tmp, strlen(tmp));
wxUint16 code;
wxUint16 len;
void *buf;
// use the actual stream for reading
i = m_contentStream;
/* Now read the contents, and try to get the needed information */
// First 4 Bytes are Version information, skip
i->SeekI(4);
while (!i->Eof())
{
// Read #SYSTEM-Code and length
i->Read(&code, 2);
code = wxUINT16_SWAP_ON_BE( code ) ;
i->Read(&len, 2);
len = wxUINT16_SWAP_ON_BE( len ) ;
// data
buf = malloc(len);
i->Read(buf, len);
switch (code)
{
case 0: // CONTENTS_FILE
if (len)
{
tmp = "Contents file=";
hhc=true;
}
break;
case 1: // INDEX_FILE
tmp = "Index file=";
hhk = true;
break;
case 2: // DEFAULT_TOPIC
tmp = "Default Topic=";
break;
case 3: // TITLE
tmp = "Title=";
break;
// case 6: // COMPILED_FILE
// tmp = "Compiled File=";
// break;
case 7: // COMPILED_FILE
tmp = "Binary Index=YES\r\n";
out->Write( (const void *) tmp, strlen(tmp));
tmp = NULL;
break;
case 4: // STRUCT SYSTEM INFO
tmp = NULL ;
if ( len >= 28 )
{
char *structptr = (char*) buf ;
// LCID at position 0
wxUint32 dummy = *((wxUint32 *)(structptr+0)) ;
wxUint32 lcid = wxUINT32_SWAP_ON_BE( dummy ) ;
char msg[64];
int len = sprintf(msg, "Language=0x%X\r\n", lcid) ;
if (len > 0)
out->Write(msg, len) ;
}
break ;
default:
tmp=NULL;
}
if (tmp)
{
out->Write((const void *) tmp, strlen(tmp));
out->Write(buf, strlen((char*)buf));
out->Write("\r\n", 2);
}
free(buf);
buf=NULL;
}
// Free the old data which wont be used any more
delete m_contentStream;
if (m_content)
free (m_content);
// Now add entries which are missing
if ( !hhc && m_chm->Contains(wxT("*.hhc")) )
{
tmp = "Contents File=*.hhc\r\n";
out->Write((const void *) tmp, strlen(tmp));
}
if ( !hhk && m_chm->Contains(wxT("*.hhk")) )
{
tmp = "Index File=*.hhk\r\n";
out->Write((const void *) tmp, strlen(tmp));
}
// Now copy the Data from the memory
out->SeekO(0, wxFromEnd);
m_size = out->TellO();
out->SeekO(0, wxFromStart);
m_content = (char *) malloc (m_size+1);
out->CopyTo(m_content, m_size);
m_content[m_size]='\0';
m_size++;
m_contentStream = new wxMemoryInputStream(m_content, m_size);
delete out;
}
}
/**
* Creates a Stream pointing to a virtual file in
* the current archive
*/
bool wxChmInputStream::CreateFileStream(const wxString& pattern)
{
wxFileInputStream * fin;
wxString tmpfile = wxFileName::CreateTempFileName(wxT("chmstrm"));
if ( tmpfile.empty() )
{
wxLogError(_("Could not create temporary file '%s'"), tmpfile);
return false;
}
// try to extract the file
if ( m_chm->Extract(pattern, tmpfile) <= 0 )
{
wxLogError(_("Extraction of '%s' into '%s' failed."),
pattern, tmpfile);
if ( wxFileExists(tmpfile) )
wxRemoveFile(tmpfile);
return false;
}
else
{
// Open a filestream to extracted file
fin = new wxFileInputStream(tmpfile);
if (!fin->IsOk())
return false;
m_size = fin->GetSize();
m_content = (char *) malloc(m_size+1);
fin->Read(m_content, m_size);
m_content[m_size]='\0';
wxRemoveFile(tmpfile);
delete fin;
m_contentStream = new wxMemoryInputStream (m_content, m_size);
return m_contentStream->IsOk();
}
}
// ----------------------------------------------------------------------------
// wxChmFSHandler
// ----------------------------------------------------------------------------
class wxChmFSHandler : public wxFileSystemHandler
{
public:
/// Constructor and Destructor
wxChmFSHandler();
virtual ~wxChmFSHandler() wxOVERRIDE;
/// Is able to open location?
virtual bool CanOpen(const wxString& location) wxOVERRIDE;
/// Open a file
virtual wxFSFile* OpenFile(wxFileSystem& fs, const wxString& location) wxOVERRIDE;
/// Find first occurrence of spec
virtual wxString FindFirst(const wxString& spec, int flags = 0) wxOVERRIDE;
/// Find next occurrence of spec
virtual wxString FindNext() wxOVERRIDE;
private:
int m_lasterror;
wxString m_pattern;
wxString m_found;
wxChmTools * m_chm;
};
wxChmFSHandler::wxChmFSHandler() : wxFileSystemHandler()
{
m_lasterror=0;
m_chm=NULL;
}
wxChmFSHandler::~wxChmFSHandler()
{
if (m_chm)
delete m_chm;
}
bool wxChmFSHandler::CanOpen(const wxString& location)
{
wxString p = GetProtocol(location);
return (p == wxT("chm")) &&
(GetProtocol(GetLeftLocation(location)) == wxT("file"));
}
wxFSFile* wxChmFSHandler::OpenFile(wxFileSystem& WXUNUSED(fs),
const wxString& location)
{
wxString right = GetRightLocation(location);
wxString left = GetLeftLocation(location);
wxInputStream *s;
int index;
if ( GetProtocol(left) != wxT("file") )
{
wxLogError(_("CHM handler currently supports only local files!"));
return NULL;
}
// Work around javascript
wxString tmp = wxString(right);
if ( tmp.MakeLower().Contains(wxT("javascipt")) && tmp.Contains(wxT("\'")) )
{
right = right.AfterFirst(wxT('\'')).BeforeLast(wxT('\''));
}
// now work on the right location
if (right.Contains(wxT("..")))
{
right = wxFileName(right).GetAbsolutePath(wxT("/"));
}
// a workaround for absolute links to root
if ( (index=right.Index(wxT("//"))) != wxNOT_FOUND )
{
right=wxString(right.Mid(index+1));
wxLogWarning(_("Link contained '//', converted to absolute link."));
}
wxFileName leftFilename = wxFileSystem::URLToFileName(left);
if (!leftFilename.FileExists())
return NULL;
// Open a stream to read the content of the chm-file
s = new wxChmInputStream(leftFilename.GetFullPath(), right, true);
if ( s )
{
return new wxFSFile(s,
left + wxT("#chm:") + right,
wxEmptyString,
GetAnchor(location),
wxDateTime(leftFilename.GetModificationTime()));
}
delete s;
return NULL;
}
/**
* Doku see wxFileSystemHandler
*/
wxString wxChmFSHandler::FindFirst(const wxString& spec, int WXUNUSED(flags))
{
wxString right = GetRightLocation(spec);
wxString left = GetLeftLocation(spec);
wxString nativename = wxFileSystem::URLToFileName(left).GetFullPath();
if ( GetProtocol(left) != wxT("file") )
{
wxLogError(_("CHM handler currently supports only local files!"));
return wxEmptyString;
}
m_chm = new wxChmTools(wxFileName(nativename));
m_pattern = right.AfterLast(wxT('/'));
wxString m_found = m_chm->Find(m_pattern);
// now fake around hhp-files which are not existing in projects...
if (m_found.empty() &&
m_pattern.Contains(wxT(".hhp")) &&
!m_pattern.Contains(wxT(".hhp.cached")))
{
m_found.Printf(wxT("%s#chm:%s.hhp"),
left, m_pattern.BeforeLast(wxT('.')));
}
return m_found;
}
wxString wxChmFSHandler::FindNext()
{
if (m_pattern.empty())
return wxEmptyString;
else
return m_chm->Find(m_pattern, m_found);
}
// ---------------------------------------------------------------------------
// wxModule to register CHM handler
// ---------------------------------------------------------------------------
class wxChmSupportModule : public wxModule
{
wxDECLARE_DYNAMIC_CLASS(wxChmSupportModule);
public:
virtual bool OnInit() wxOVERRIDE
{
wxFileSystem::AddHandler(new wxChmFSHandler);
return true;
}
virtual void OnExit() wxOVERRIDE {}
}
;
wxIMPLEMENT_DYNAMIC_CLASS(wxChmSupportModule, wxModule);
#endif // wxUSE_LIBMSPACK