Moved xml_escape into a separate file.
authorBen Hutchings <ben@decadent.org.uk>
Sun, 26 Feb 2006 19:45:47 +0000 (19:45 +0000)
committerBen Hutchings <ben@decadent.org.uk>
Sun, 2 Nov 2008 23:39:57 +0000 (23:39 +0000)
Changed voblist implementation to expect a real XML document and to resolve VOB filenames relative to the voblist.

12 files changed:
INSTALL
Makefile
README
TODO
debian/control
generate_dvd.cpp
generate_dvd.hpp
vob_list.cpp [new file with mode: 0644]
vob_list.hpp [new file with mode: 0644]
webdvd.cpp
xml_utils.cpp [new file with mode: 0644]
xml_utils.hpp [new file with mode: 0644]

diff --git a/INSTALL b/INSTALL
index 4f87e47..666e2d1 100644 (file)
--- a/INSTALL
+++ b/INSTALL
@@ -4,9 +4,10 @@ Building WebDVD
 WebDVD is written in C++ and requires a recent C++ compiler e.g. g++
 3.3.
 
-It requires headers and libraries for Boost, gtkmm and Mozilla.  I
-have developed and tested it with Boost 1.32, gtkmm 2.2.12 and Mozilla
-1.7.8 but it may well work with earlier or later versions of these.
+It requires headers and libraries for Boost, gtkmm, Mozilla and expat.
+I have developed and tested it with Boost 1.32, gtkmm 2.2.12, Mozilla
+1.7.8 and expat 1.95.8 but it may well work with earlier or later
+versions of these.
 
 I use Debian Linux and have not yet attempted to build it on other
 systems, but it should work on most modern Unix-like systems.
index 37018f6..1383056 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -20,7 +20,7 @@ CPPFLAGS := -D_REENTRANT
 # instances are only ever called by the Release function which is virtual.
 CXXFLAGS := -Wall -Wno-non-virtual-dtor
 LDFLAGS := -lpthread $(shell pkg-config --libs gtkmm-2.0 mozilla-gtkmozembed) \
-           -Wl,-rpath -Wl,$(moz_lib_dir)
+           -Wl,-rpath -Wl,$(moz_lib_dir) -lexpat
 
 ifdef NDEBUG
     CPPFLAGS += -DNDEBUG
@@ -33,7 +33,7 @@ endif
 cxxsources := \
     auto_proc.cpp browser_widget.cpp child_iterator.cpp generate_dvd.cpp   \
     link_iterator.cpp pixbufs.cpp style_sheets.cpp temp_file.cpp video.cpp \
-    webdvd.cpp x_frame_buffer.cpp xpcom_support.cpp
+    vob_list.cpp webdvd.cpp x_frame_buffer.cpp xml_utils.cpp xpcom_support.cpp
 csources := jquant2.c
 
 webdvd : $(cxxsources:.cpp=.o) $(csources:.c=.o)
@@ -60,7 +60,7 @@ webdvd.% \
                   -DMOZ_VERSION_MINOR=$(moz_version_minor)           \
                   -DMOZ_VERSION_PATCHLEVEL=$(moz_version_patchlevel)
 
-browser_widget.% generate_dvd.% pixbufs.% temp_file.% webdvd.% \
+browser_widget.% generate_dvd.% pixbufs.% temp_file.% vob_list.% webdvd.% \
     : CPPFLAGS += $(shell pkg-config --cflags gtkmm-2.0)
 
 browser_widget.% child_iterator.o link_iterator.% style_sheets.% webdvd.% \
diff --git a/README b/README
index e075060..3cd0e12 100644 (file)
--- a/README
+++ b/README
@@ -13,6 +13,7 @@ Requirements
 WebDVD depends on the following software:
 
 - dvdauthor
+- expat 1.x
 - Gtkmm 2.0
 - mjpegtools
 - Mozilla 1.7.x (later versions may work but are untested)
@@ -40,18 +41,20 @@ Linking to video
 You can link directly to local MPEG video files whose names end in
 ".vob".  If you wish to combine multiple files into a single video
 sequence ("title" in DVD terminology) or to add chapter marks to a
-video sequence, create a text file whose name ends in ".voblist" and
-containg <vob> elements as described in the dvdauthor manual page, and
-link to that.  Note that these list files are currently included
-directly in the control file that WebDVD passes to dvdauthor, which
-means that file names in them will be resolved relative to the current
-directory rather than the directory containing the list file.  This is
-probably a bug.
+video sequence, create and link to a VOB-list file (explained below)
+whose name ends in ".voblist".
 
 You can link to a title and begin playback at the beginning of a
 specific chapter by adding "#" and then the chapter number to the end
 of the URL.
 
+VOB-lists
+
+A VOB-list file is an XML file with the document element <vob-list>
+and containing <vob> elements as described in the dvdauthor manual
+page.  The file names in a VOB-list file are resolved relative to the
+directory containing the list file.
+
 Video standards
 
 By default, webdvd generates PAL/SECAM video.  If you wish to produce
diff --git a/TODO b/TODO
index 6e74f0e..e60b7b6 100644 (file)
--- a/TODO
+++ b/TODO
@@ -1,5 +1,4 @@
 Priority 1 (highest)
-Fix resolution of relative filenames in VOB lists.
 Provide a means to specify video aspect and menu & audio language.
 Suppress Mozilla error dialogs when processing (file not found etc).
 
index 4bd2e13..af7a4dc 100644 (file)
@@ -2,7 +2,7 @@ Source: webdvd
 Maintainer: Ben Hutchings <ben@decadentplace.org.uk>
 Section: graphics
 Priority: extra
-Build-Depends: debhelper (>=4), libboost-dev, libgtkmm2.0-dev, mozilla-dev
+Build-Depends: debhelper (>=4), libboost-dev, libgtkmm2.0-dev, mozilla-dev, libexpat1-dev
 Standards-Version: 3.6.2
 
 Package: webdvd
index 6144ef5..eeadc48 100644 (file)
@@ -8,6 +8,7 @@
 
 #include "dvd.hpp"
 #include "generate_dvd.hpp"
+#include "xml_utils.hpp"
 
 dvd_contents::menu::menu()
        : vob_temp(new temp_file("webdvd-vob-"))
@@ -193,8 +194,22 @@ void generate_dvd(const dvd_contents & contents,
            "    <titles>\n"
            "      <pgc>\n"
            // Record calling location.
-           "        <pre> g12 = g1; </pre>\n"
-            << contents.titles[title_num].vob_list <<
+           "        <pre> g12 = g1; </pre>\n";
+
+       for (vob_list::const_iterator it = contents.titles[title_num].begin(),
+                end = contents.titles[title_num].end();
+            it != end;
+            ++it)
+       {
+           file << "        <vob file='" << xml_escape(it->file) << "'";
+           if (!it->chapters.empty())
+               file << " chapters='" << xml_escape(it->chapters) << "'";
+           if (!it->pause.empty())
+               file << " pause='" << xml_escape(it->pause) << "'";
+           file << "/>\n";
+       }
+
+       file <<
            // If the menu location has not been changed during
            // the title, set the location to be the following
            // button in the menu.  In any case, return to some
index 8ce8f7b..f77d7da 100644 (file)
@@ -10,6 +10,7 @@
 #include <boost/shared_ptr.hpp>
 
 #include "temp_file.hpp"
+#include "vob_list.hpp"
 
 // Description of menus and titles to go on a DVD.
 
@@ -54,21 +55,8 @@ struct dvd_contents
        std::vector<pgc_ref> entries;
     };
 
-    // Title definition.  This is currently just an XML fragment
-    // consisting of one of more <vob> elements.  It is included
-    // directly within the corresponding <pgc> element in the file
-    // passed to dvdauthor.
-    struct title
-    {
-       explicit title(const std::string & vob_list)
-               : vob_list(vob_list)
-           {}
-
-       std::string vob_list;
-    };
-
     std::vector<menu> menus;
-    std::vector<title> titles;
+    std::vector<vob_list> titles;
 };
 
 // Use dvdauthor to generate a DVD filesystem with the given contents.
diff --git a/vob_list.cpp b/vob_list.cpp
new file mode 100644 (file)
index 0000000..086e1a0
--- /dev/null
@@ -0,0 +1,189 @@
+// Copyright 2006 Ben Hutchings <ben@decadentplace.org.uk>.
+// See the file "COPYING" for licence details.
+
+#include <cerrno>
+#include <cstring>
+#include <sstream>
+
+#include <unistd.h>
+#include <fcntl.h>
+
+#include <expat.h>
+
+#include <glibmm/fileutils.h>
+#include <glibmm/miscutils.h>
+
+#include "auto_fd.hpp"
+#include "auto_handle.hpp"
+#include "vob_list.hpp"
+
+namespace
+{
+    struct xml_parser_closer
+    {
+       void operator()(XML_Parser parser) const
+           {
+               if (parser)
+                   XML_ParserFree(parser);
+           }
+    };
+
+    struct xml_parser_factory
+    {
+       XML_Parser operator()() const
+           {
+               return NULL;
+           }
+    };
+
+    typedef auto_handle<XML_Parser, xml_parser_closer, xml_parser_factory>
+    auto_xml_parser;
+
+    struct parse_context
+    {
+       parse_context(XML_Parser parser,
+                     const std::string & list_path,
+                     vob_list & list)
+               : parser(parser),
+                 list_path(list_path),
+                 base_path(Glib::path_get_dirname(list_path)),
+                 list(list),
+                 level(0)
+           {}
+       XML_Parser parser;
+       const std::string & list_path;
+       std::string base_path;
+       vob_list & list;
+       std::auto_ptr<xml_error> parse_error;
+       unsigned level;
+    };
+
+    void start_element_handler(void * user_data,
+                              const char * name,
+                              const char ** attributes)
+    {
+       parse_context & context = *static_cast<parse_context *>(user_data);
+
+       if (context.level == 0 && std::strcmp(name, "vob-list") == 0)
+       {
+           // We don't expect (and will ignore) any attributes.
+       }
+       else if (context.level == 1 && std::strcmp(name, "vob") == 0)
+       {
+           vob_ref ref;
+
+           while (attributes[0] != NULL)
+           {
+               if (std::strcmp(attributes[0], "file") == 0)
+               {
+                   ref.file = attributes[1];
+                   if (!Glib::path_is_absolute(ref.file))
+                       ref.file = Glib::build_filename(context.base_path,
+                                                       ref.file);
+               }
+               else if (std::strcmp(attributes[0], "chapters") == 0)
+               {
+                   ref.chapters = attributes[1];
+               }
+               else if (std::strcmp(attributes[0], "pause") == 0)
+               {
+                   ref.pause = attributes[1];
+               }
+
+               attributes += 2;
+           }
+
+           if (ref.file.empty())
+           {
+               context.parse_error.reset(
+                   new xml_error(
+                       context.list_path,
+                       XML_GetCurrentLineNumber(context.parser),
+                       "<vob> element missing file attribute"));
+           }
+           else
+           {
+               context.list.push_back(ref);
+           }
+       }
+       else // not root <vob-list> or child <vob>
+       {
+           context.parse_error.reset(
+               new xml_error(
+                   context.list_path,
+                   XML_GetCurrentLineNumber(context.parser),
+                   std::string("unexpected element: <").append(name)
+                   .append(">")));
+       }
+
+       ++context.level;
+    }
+
+    void end_element_handler(void * user_data,
+                            const char * name)
+    {
+       parse_context & context = *static_cast<parse_context *>(user_data);
+       --context.level;
+    }
+}
+
+vob_list read_vob_list(const std::string & path)
+{
+    vob_list result;
+
+    auto_fd fd(open(path.c_str(), O_RDONLY, 0));
+    if (fd.get() < 0)
+       // FIXME: look up proper error code
+       throw Glib::FileError(Glib::FileError::FAILED,
+                             std::strerror(errno));
+
+    auto_xml_parser parser(XML_ParserCreate(NULL));
+    if (parser.get() == NULL)
+       throw std::bad_alloc(); // any other reason?
+
+    parse_context context(parser.get(), path, result);
+    XML_SetUserData(parser.get(), &context);
+    XML_SetStartElementHandler(parser.get(), start_element_handler);
+    XML_SetEndElementHandler(parser.get(), end_element_handler);
+
+    for (;;)
+    {
+       static const int buffer_size = 1024;
+       void * buffer = XML_GetBuffer(parser.get(), buffer_size);
+       if (buffer == NULL)
+           throw std::bad_alloc();
+       int read_size = read(fd.get(), buffer, buffer_size);
+       if (read_size < 0)
+           // FIXME: look up proper error code
+           throw Glib::FileError(Glib::FileError::FAILED,
+                                 std::strerror(errno));
+       bool is_final = read_size < buffer_size;
+       if (XML_ParseBuffer(parser.get(), read_size, is_final)
+           == XML_STATUS_ERROR)
+           throw xml_error(path,
+                           XML_GetCurrentLineNumber(parser.get()),
+                           XML_ErrorString(XML_GetErrorCode(parser.get())));
+       if (context.parse_error.get())
+           throw *context.parse_error;
+       if (is_final)
+           break;
+    }
+
+    return result;
+}
+
+namespace
+{
+    std::string make_xml_error_message(const std::string & path, int line,
+                                      const std::string & message)
+    {
+       std::ostringstream os;
+       os << path << ":" << line << ": " << message;
+       return os.str();
+    }
+}
+
+xml_error::xml_error(const std::string & path, int line,
+                    const std::string & message)
+       : std::runtime_error(make_xml_error_message(path, line, message))
+{}
diff --git a/vob_list.hpp b/vob_list.hpp
new file mode 100644 (file)
index 0000000..aaa58b5
--- /dev/null
@@ -0,0 +1,27 @@
+// Copyright 2006 Ben Hutchings <ben@decadentplace.org.uk>.
+// See the file "COPYING" for licence details.
+
+#ifndef INC_VOB_LIST_HPP
+#define INC_VOB_LIST_HPP
+
+#include <list>
+#include <stdexcept>
+#include <string>
+
+struct vob_ref
+{
+    std::string file;          // file name (absolute, resolved rel. to list)
+    std::string chapters;      // chapters attribute, unmodified
+    std::string pause;         // pause attribute, unmodified
+};
+
+typedef std::list<vob_ref> vob_list;
+
+vob_list read_vob_list(const std::string & file_name);
+
+struct xml_error : public std::runtime_error
+{
+    xml_error(const std::string & path, int line, const std::string & message);
+};
+
+#endif // !INC_VOB_LIST_HPP
index 8019007..43f54c7 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright 2005 Ben Hutchings <ben@decadentplace.org.uk>.
+// Copyright 2005-6 Ben Hutchings <ben@decadentplace.org.uk>.
 // See the file "COPYING" for licence details.
 
 #include <cassert>
@@ -60,6 +60,7 @@
 #include "temp_file.hpp"
 #include "video.hpp"
 #include "x_frame_buffer.hpp"
+#include "xml_utils.hpp"
 #include "xpcom_support.hpp"
 
 using xpcom_support::check;
@@ -144,35 +145,6 @@ namespace
     }
 
 
-    std::string xml_escape(const std::string & str)
-    {
-       std::string result;
-       std::size_t begin = 0;
-
-       for (;;)
-       {
-           std::size_t end = str.find_first_of("\"&'<>", begin);
-           result.append(str, begin, end - begin);
-           if (end == std::string::npos)
-               return result;
-
-           const char * entity = NULL;
-           switch (str[end])
-           {
-           case '"':  entity = "&quot;"; break;
-           case '&':  entity = "&amp;";  break;
-           case '\'': entity = "&apos;"; break;
-           case '<':  entity = "&lt;";   break;
-           case '>':  entity = "&gt;";   break;
-           }
-           assert(entity);
-           result.append(entity);
-
-           begin = end + 1;
-       }
-    }
-
-    
     class webdvd_window : public Gtk::Window
     {
     public:
@@ -270,32 +242,30 @@ namespace
        else
        {
            Glib::ustring hostname;
-           std::string filename(Glib::filename_from_uri(uri, hostname));
+           std::string path(Glib::filename_from_uri(uri, hostname));
            // FIXME: Should check the hostname
 
-           std::string vob_list;
+           vob_list list;
 
            // Store a reference to a linked VOB file, or the contents
            // of a linked VOB list file.
-           if (filename.compare(filename.size() - 4, 4, ".vob") == 0)
+           if (path.compare(path.size() - 4, 4, ".vob") == 0)
            {
-               if (!Glib::file_test(filename, Glib::FILE_TEST_IS_REGULAR))
+               if (!Glib::file_test(path, Glib::FILE_TEST_IS_REGULAR))
                    throw std::runtime_error(
-                       filename + " is missing or not a regular file");
-               vob_list
-                   .append("<vob file='")
-                   .append(xml_escape(filename))
-                   .append("'/>\n");
+                       path + " is missing or not a regular file");
+               vob_ref ref;
+               ref.file = path;
+               list.push_back(ref);
            }
            else
            {
-               assert(filename.compare(filename.size() - 8, 8, ".voblist")
-                      == 0);
-               // TODO: Validate the file contents
-               vob_list.assign(Glib::file_get_contents(filename));
+               assert(path.compare(path.size() - 8, 8, ".voblist") == 0);
+               read_vob_list(path).swap(list);
            }
 
-           contents_.titles.push_back(dvd_contents::title(vob_list));
+           contents_.titles.resize(contents_.titles.size() + 1);
+           contents_.titles.back().swap(list);
            return next_title;
        }
     }
diff --git a/xml_utils.cpp b/xml_utils.cpp
new file mode 100644 (file)
index 0000000..981feba
--- /dev/null
@@ -0,0 +1,35 @@
+// Copyright 2005-6 Ben Hutchings <ben@decadentplace.org.uk>.
+// See the file "COPYING" for licence details.
+
+#include <cassert>
+#include <cstddef>
+
+#include "xml_utils.hpp"
+
+std::string xml_escape(const std::string & str)
+{
+    std::string result;
+    std::size_t begin = 0;
+
+    for (;;)
+    {
+       std::size_t end = str.find_first_of("\"&'<>", begin);
+       result.append(str, begin, end - begin);
+       if (end == std::string::npos)
+           return result;
+
+       const char * entity = NULL;
+       switch (str[end])
+       {
+       case '"':  entity = "&quot;"; break;
+       case '&':  entity = "&amp;";  break;
+       case '\'': entity = "&apos;"; break;
+       case '<':  entity = "&lt;";   break;
+       case '>':  entity = "&gt;";   break;
+       }
+       assert(entity);
+       result.append(entity);
+
+       begin = end + 1;
+    }
+}
diff --git a/xml_utils.hpp b/xml_utils.hpp
new file mode 100644 (file)
index 0000000..e61e387
--- /dev/null
@@ -0,0 +1,11 @@
+// Copyright 2006 Ben Hutchings <ben@decadentplace.org.uk>.
+// See the file "COPYING" for licence details.
+
+#ifndef INC_XML_UTILS_HPP
+#define INC_XML_UTILS_HPP
+
+#include <string>
+
+std::string xml_escape(const std::string & str);
+
+#endif // !INC_XML_UTILS_HPP