From c13714f6498df33e02635421354f5fb88a60eb3d Mon Sep 17 00:00:00 2001
From: Ben Hutchings <ben@decadent.org.uk>
Date: Sun, 26 Feb 2006 19:45:47 +0000
Subject: [PATCH] Moved xml_escape into a separate file. Changed voblist
 implementation to expect a real XML document and to resolve VOB filenames
 relative to the voblist.

---
 INSTALL          |   7 +-
 Makefile         |   6 +-
 README           |  17 +++--
 TODO             |   1 -
 debian/control   |   2 +-
 generate_dvd.cpp |  19 ++++-
 generate_dvd.hpp |  16 +---
 vob_list.cpp     | 189 +++++++++++++++++++++++++++++++++++++++++++++++
 vob_list.hpp     |  27 +++++++
 webdvd.cpp       |  58 ++++-----------
 xml_utils.cpp    |  35 +++++++++
 xml_utils.hpp    |  11 +++
 12 files changed, 313 insertions(+), 75 deletions(-)
 create mode 100644 vob_list.cpp
 create mode 100644 vob_list.hpp
 create mode 100644 xml_utils.cpp
 create mode 100644 xml_utils.hpp

diff --git a/INSTALL b/INSTALL
index 4f87e47..666e2d1 100644
--- 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.
diff --git a/Makefile b/Makefile
index 37018f6..1383056 100644
--- 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
--- 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
--- 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).
 
diff --git a/debian/control b/debian/control
index 4bd2e13..af7a4dc 100644
--- a/debian/control
+++ b/debian/control
@@ -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
diff --git a/generate_dvd.cpp b/generate_dvd.cpp
index 6144ef5..eeadc48 100644
--- a/generate_dvd.cpp
+++ b/generate_dvd.cpp
@@ -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
diff --git a/generate_dvd.hpp b/generate_dvd.hpp
index 8ce8f7b..f77d7da 100644
--- a/generate_dvd.hpp
+++ b/generate_dvd.hpp
@@ -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
index 0000000..086e1a0
--- /dev/null
+++ b/vob_list.cpp
@@ -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
index 0000000..aaa58b5
--- /dev/null
+++ b/vob_list.hpp
@@ -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
diff --git a/webdvd.cpp b/webdvd.cpp
index 8019007..43f54c7 100644
--- a/webdvd.cpp
+++ b/webdvd.cpp
@@ -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
index 0000000..981feba
--- /dev/null
+++ b/xml_utils.cpp
@@ -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
index 0000000..e61e387
--- /dev/null
+++ b/xml_utils.hpp
@@ -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
-- 
2.39.5