]> git.mxchange.org Git - simgear.git/commitdiff
SVN read-only client code using our HTTP engine.
authorJames Turner <zakalawe@mac.com>
Thu, 25 Oct 2012 15:45:59 +0000 (16:45 +0100)
committerJames Turner <zakalawe@mac.com>
Sun, 9 Jun 2013 18:19:03 +0000 (19:19 +0100)
simgear/io/CMakeLists.txt
simgear/io/DAVMultiStatus.cxx [new file with mode: 0644]
simgear/io/DAVMultiStatus.hxx [new file with mode: 0644]
simgear/io/SVNDirectory.cxx [new file with mode: 0644]
simgear/io/SVNDirectory.hxx [new file with mode: 0644]
simgear/io/SVNReportParser.cxx [new file with mode: 0644]
simgear/io/SVNReportParser.hxx [new file with mode: 0644]
simgear/io/SVNRepository.cxx [new file with mode: 0644]
simgear/io/SVNRepository.hxx [new file with mode: 0644]
simgear/io/http_svn.cxx [new file with mode: 0644]

index 8170b2265bebcd2a3b23b3a494164b963df33c58..963e043679ae4ee36464431c9adb6fc51a1bbad4 100644 (file)
@@ -16,6 +16,10 @@ set(HEADERS
     sg_socket_udp.hxx
     HTTPClient.hxx
     HTTPRequest.hxx
+    DAVMultiStatus.hxx
+    SVNRepository.hxx
+    SVNDirectory.hxx
+    SVNReportParser.hxx
     )
 
 set(SOURCES
@@ -32,11 +36,28 @@ set(SOURCES
     sg_socket_udp.cxx
     HTTPClient.cxx
     HTTPRequest.cxx
+    DAVMultiStatus.cxx
+    SVNRepository.cxx
+    SVNDirectory.cxx
+    SVNReportParser.cxx
     )
 
 simgear_component(io io "${SOURCES}" "${HEADERS}")
 
 if(ENABLE_TESTS)
+    
+if (SIMGEAR_SHARED)
+    set(TEST_LIBS SimGearCore)
+else()
+    set(TEST_LIBS SimGearCore
+            ${CMAKE_THREAD_LIBS_INIT}
+            ${WINSOCK_LIBRARY}
+            ${ZLIB_LIBRARY}
+            ${RT_LIBRARY})
+endif()
+
+add_executable(http_svn http_svn.cxx)
+target_link_libraries(http_svn ${TEST_LIBS})
 
 add_executable(test_sock socktest.cxx)
 target_link_libraries(test_sock ${TEST_LIBS})
diff --git a/simgear/io/DAVMultiStatus.cxx b/simgear/io/DAVMultiStatus.cxx
new file mode 100644 (file)
index 0000000..54c2387
--- /dev/null
@@ -0,0 +1,385 @@
+// DAVMultiStatus.cxx -- parser for WebDAV MultiStatus XML data
+//
+// Copyright (C) 2012  James Turner <zakalawe@mac.com>
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License as
+// published by the Free Software Foundation; either version 2 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+#include "DAVMultiStatus.hxx"
+
+#include <iostream>
+#include <cstring>
+#include <cassert>
+#include <algorithm>
+#include <sstream>
+
+#include <boost/foreach.hpp>
+
+#include "simgear/debug/logstream.hxx"
+#include "simgear/xml/xmlparse.h"
+#include "simgear/misc/strutils.hxx"
+#include "simgear/structure/exception.hxx"
+
+using std::cout;
+using std::cerr;
+using std::endl;
+using std::string;
+
+using namespace simgear;
+
+#define DAV_NS "DAV::"
+#define SUBVERSION_DAV_NS "http://subversion.tigris.org/xmlns/dav/"
+
+const char* DAV_MULTISTATUS_TAG = DAV_NS "multistatus";
+const char* DAV_RESPONSE_TAG = DAV_NS "response";
+const char* DAV_PROPSTAT_TAG = DAV_NS "propstat";
+const char* DAV_PROP_TAG = DAV_NS "prop";
+
+const char* DAV_HREF_TAG = DAV_NS "href";
+const char* DAV_RESOURCE_TYPE_TAG = DAV_NS "resourcetype";
+const char* DAV_CONTENT_TYPE_TAG = DAV_NS "getcontenttype";
+const char* DAV_CONTENT_LENGTH_TAG = DAV_NS "getcontentlength";
+const char* DAV_VERSIONNAME_TAG = DAV_NS "version-name";
+const char* DAV_COLLECTION_TAG = DAV_NS "collection";
+const char* DAV_VCC_TAG = DAV_NS "version-controlled-configuration";
+
+const char* SUBVERSION_MD5_CHECKSUM_TAG = SUBVERSION_DAV_NS ":md5-checksum";
+
+DAVResource::DAVResource(const string& href) :
+  _type(Unknown),
+  _url(href),
+  _container(NULL)
+{
+    assert(!href.empty()); 
+}
+
+void DAVResource::setVersionName(const std::string& aVersion)
+{
+  _versionName = aVersion;
+}
+
+void DAVResource::setVersionControlledConfiguration(const std::string& vcc)
+{
+    _vcc = vcc;
+}
+
+void DAVResource::setMD5(const std::string& md5Hex)
+{
+    _md5 = md5Hex;
+}
+
+std::string DAVResource::name() const
+{
+    string::size_type index = _url.rfind('/');
+    if (index != string::npos) {
+        return _url.substr(index + 1);
+    }
+    
+    throw sg_exception("bad DAV resource HREF:" + _url);
+}
+
+////////////////////////////////////////////////////////////////////////////
+
+DAVCollection::DAVCollection(const string& href) :
+  DAVResource(href)
+{
+  _type = DAVResource::Collection;
+}
+
+DAVCollection::~DAVCollection()
+{
+  BOOST_FOREACH(DAVResource* c, _contents) {
+    delete c;
+  }
+}
+
+void DAVCollection::addChild(DAVResource *res)
+{
+  assert(res);
+  if (res->container() == this) {
+    return;
+  }
+  
+  assert(res->container() == NULL);
+  assert(std::find(_contents.begin(), _contents.end(), res) == _contents.end());
+  
+  if (!strutils::starts_with(res->url(), _url)) {
+      std::cerr << "us: " << _url << std::endl;
+      std::cerr << "child:" << res->url() << std::endl;
+      
+  }
+  
+  assert(strutils::starts_with(res->url(), _url));
+  assert(childWithUrl(res->url()) == NULL);
+  
+  res->_container = this;
+  _contents.push_back(res);
+}
+
+void DAVCollection::removeChild(DAVResource* res)
+{
+  assert(res);
+  assert(res->container() == this);
+  
+  res->_container = NULL;
+  DAVResourceList::iterator it = std::find(_contents.begin(), _contents.end(), res);
+  assert(it != _contents.end());
+  _contents.erase(it);
+}
+
+DAVCollection*
+DAVCollection::createChildCollection(const std::string& name)
+{
+    DAVCollection* child = new DAVCollection(urlForChildWithName(name));
+    addChild(child);
+    return child;
+}
+
+DAVResourceList DAVCollection::contents() const
+{
+  return _contents;
+}
+
+DAVResource* DAVCollection::childWithUrl(const string& url) const
+{
+  if (url.empty())
+    return NULL;
+  
+  BOOST_FOREACH(DAVResource* c, _contents) {
+    if (c->url() == url) {
+      return c;
+    }
+  }
+  
+  return NULL;
+}
+
+DAVResource* DAVCollection::childWithName(const string& name) const
+{
+  return childWithUrl(urlForChildWithName(name));
+}
+
+std::string DAVCollection::urlForChildWithName(const std::string& name) const
+{
+  return url() + "/" + name;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+
+class DAVMultiStatus::DAVMultiStatusPrivate
+{
+public:
+  DAVMultiStatusPrivate() :
+  parserInited(false)
+  {
+    rootResource = NULL;
+  }
+  
+  void startElement (const char * name)
+  {
+    if (tagStack.empty()) {
+      if (strcmp(name, DAV_MULTISTATUS_TAG)) {
+        SG_LOG(SG_IO, SG_WARN, "root element is not " <<
+               DAV_MULTISTATUS_TAG << ", got:" << name);
+      } else {
+        
+      }
+    } else {
+      // not at the root element
+      if (tagStack.back() == DAV_MULTISTATUS_TAG) {
+        if (strcmp(name, DAV_RESPONSE_TAG)) {
+          SG_LOG(SG_IO, SG_WARN, "multistatus child is not response: saw:"
+                 << name);
+        }
+      }
+      
+      if (tagStack.back() == DAV_RESOURCE_TYPE_TAG) {
+        if (!strcmp(name, DAV_COLLECTION_TAG)) {
+          currentElementType = DAVResource::Collection;
+        } else {
+          currentElementType = DAVResource::Unknown;
+        }
+      }
+    }
+    
+    tagStack.push_back(name);
+    if (!strcmp(name, DAV_RESPONSE_TAG)) {
+      currentElementType = DAVResource::Unknown;
+      currentElementUrl.clear();
+      currentElementMD5.clear();
+      currentVersionName.clear();
+      currentVCC.clear();
+    }
+  }
+  
+  void endElement (const char * name)
+  {
+    assert(tagStack.back() == name);
+    tagStack.pop_back();
+    
+    if (!strcmp(name, DAV_RESPONSE_TAG)) {
+      // finish complete response
+      currentElementUrl = strutils::strip(currentElementUrl);
+      
+      DAVResource* res = NULL;
+      if (currentElementType == DAVResource::Collection) {
+        DAVCollection* col = new DAVCollection(currentElementUrl);
+        res = col;
+      } else {
+        res = new DAVResource(currentElementUrl);
+      }
+      
+      res->setVersionName(strutils::strip(currentVersionName));
+      res->setVersionControlledConfiguration(currentVCC);
+      if (rootResource &&
+          strutils::starts_with(currentElementUrl, rootResource->url()))
+      {
+        static_cast<DAVCollection*>(rootResource)->addChild(res);
+      }
+      
+      if (!rootResource) {
+        rootResource = res;
+      }
+    }
+  }
+  
+  void data (const char * s, int length)
+  {
+    if (tagStack.back() == DAV_HREF_TAG) {
+      if (tagN(1) == DAV_RESPONSE_TAG) {
+        currentElementUrl += string(s, length);
+      } else if (tagN(1) == DAV_VCC_TAG) {
+        currentVCC += string(s, length);
+      }
+    } else if (tagStack.back() == SUBVERSION_MD5_CHECKSUM_TAG) {
+      currentElementMD5 = string(s, length);
+    } else if (tagStack.back() == DAV_VERSIONNAME_TAG) {
+      currentVersionName = string(s, length);
+    } else if (tagStack.back() == DAV_CONTENT_LENGTH_TAG) {
+      std::istringstream is(string(s, length));
+      is >> currentElementLength;
+    }
+  }
+  
+  void pi (const char * target, const char * data) {}
+  
+  string tagN(const unsigned int n) const
+  {
+    int sz = tagStack.size();
+    if (n >= sz) {
+      return string();
+    }
+    
+    return tagStack[sz - (1 + n)];
+  }
+  
+  bool parserInited;
+  XML_Parser xmlParser;
+  DAVResource* rootResource;
+  
+  // in-flight data
+  string_list tagStack;
+  DAVResource::Type currentElementType;
+  string currentElementUrl,
+    currentVersionName,
+    currentVCC;
+  int currentElementLength;
+  string currentElementMD5;
+};
+
+
+////////////////////////////////////////////////////////////////////////
+// Static callback functions for Expat.
+////////////////////////////////////////////////////////////////////////
+
+#define VISITOR static_cast<DAVMultiStatus::DAVMultiStatusPrivate *>(userData)
+
+static void
+start_element (void * userData, const char * name, const char ** atts)
+{
+  VISITOR->startElement(name);
+}
+
+static void
+end_element (void * userData, const char * name)
+{
+  VISITOR->endElement(name);
+}
+
+static void
+character_data (void * userData, const char * s, int len)
+{
+  VISITOR->data(s, len);
+}
+
+static void
+processing_instruction (void * userData,
+                        const char * target,
+                        const char * data)
+{
+  VISITOR->pi(target, data);
+}
+
+#undef VISITOR
+
+///////////////////////////////////////////////////////////////////////////////
+
+DAVMultiStatus::DAVMultiStatus() :
+_d(new DAVMultiStatusPrivate)
+{
+  
+}
+
+DAVMultiStatus::~DAVMultiStatus()
+{
+  
+}
+
+void DAVMultiStatus::parseXML(const char* data, int size)
+{
+  if (!_d->parserInited) {
+    _d->xmlParser = XML_ParserCreateNS(0, ':');
+    XML_SetUserData(_d->xmlParser, _d.get());
+    XML_SetElementHandler(_d->xmlParser, start_element, end_element);
+    XML_SetCharacterDataHandler(_d->xmlParser, character_data);
+    XML_SetProcessingInstructionHandler(_d->xmlParser, processing_instruction);
+    _d->parserInited = true;
+  }
+  
+  if (!XML_Parse(_d->xmlParser, data, size, false)) {
+    SG_LOG(SG_IO, SG_WARN, "DAV parse error:" << XML_ErrorString(XML_GetErrorCode(_d->xmlParser))
+           << " at line:" << XML_GetCurrentLineNumber(_d->xmlParser)
+           << " column " << XML_GetCurrentColumnNumber(_d->xmlParser));
+    
+    XML_ParserFree(_d->xmlParser);
+    _d->parserInited = false;
+  }
+}
+
+void DAVMultiStatus::finishParse()
+{
+  if (_d->parserInited) {
+    XML_Parse(_d->xmlParser, NULL, 0, true);
+    XML_ParserFree(_d->xmlParser);
+  }
+  
+  _d->parserInited = false;
+}
+
+DAVResource* DAVMultiStatus::resource()
+{
+  return _d->rootResource;
+}
+
+
diff --git a/simgear/io/DAVMultiStatus.hxx b/simgear/io/DAVMultiStatus.hxx
new file mode 100644 (file)
index 0000000..862bf9d
--- /dev/null
@@ -0,0 +1,141 @@
+// DAVMultiStatus.hxx -- parser for WebDAV MultiStatus XML data
+//
+// Copyright (C) 2012  James Turner <zakalawe@mac.com>
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License as
+// published by the Free Software Foundation; either version 2 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+
+#ifndef SG_IO_DAVMULTISTATUS_HXX
+#define SG_IO_DAVMULTISTATUS_HXX
+
+#include <string>
+#include <vector>
+#include <memory> // for auto_ptr
+
+namespace simgear
+{
+
+class DAVCollection;
+
+class DAVResource
+{
+public:
+    DAVResource(const std::string& url);
+    virtual ~DAVResource() { }
+  
+    typedef enum {
+        Unknown = 0,
+        Collection = 1
+    } Type;
+        
+    const Type type() const
+        { return _type; }
+    
+    const std::string& url() const
+        { return _url; }
+
+    std::string name() const;
+
+    /**
+     * SVN servers use this field to expose the head revision
+     * of the resource, which is useful
+     */
+    const std::string& versionName() const
+      { return _versionName; }
+
+    void setVersionName(const std::string& aVersion);
+
+    DAVCollection* container() const
+        { return _container; }
+    
+    virtual bool isCollection() const
+        { return false; }
+
+    void setVersionControlledConfiguration(const std::string& vcc);
+    const std::string& versionControlledConfiguration() const
+        { return _vcc; }
+    
+    void setMD5(const std::string& md5Hex);
+    const std::string& md5() const
+        { return _md5; }
+protected:
+    friend class DAVCollection;
+  
+    Type _type;
+    std::string _url;
+    std::string _versionName;
+    std::string _vcc;
+    std::string _md5;
+    DAVCollection* _container;
+};
+
+typedef std::vector<DAVResource*> DAVResourceList;
+
+class DAVCollection : public DAVResource
+{
+public:
+  DAVCollection(const std::string& url);
+  virtual ~DAVCollection();
+  
+  DAVResourceList contents() const;
+  
+  void addChild(DAVResource* res);
+  void removeChild(DAVResource* res);
+  
+  DAVCollection* createChildCollection(const std::string& name);
+  
+  /**
+   * find the collection member with the specified URL, or return NULL
+   * if no such member of this collection exists.
+   */
+  DAVResource* childWithUrl(const std::string& url) const;
+     
+  /**
+   * find the collection member with the specified name, or return NULL
+   */
+  DAVResource* childWithName(const std::string& name) const;
+   
+  /**
+   * wrapper around URL manipulation
+   */
+  std::string urlForChildWithName(const std::string& name) const;
+  
+  virtual bool isCollection() const
+  { return true; }
+private:
+  DAVResourceList _contents;
+};
+
+class DAVMultiStatus
+{
+public:
+    DAVMultiStatus();
+    ~DAVMultiStatus();
+    
+    // incremental XML parsing
+    void parseXML(const char* data, int size);
+    
+    void finishParse();
+    
+    DAVResource* resource();
+  
+  class DAVMultiStatusPrivate;
+private:
+    std::auto_ptr<DAVMultiStatusPrivate> _d;
+};
+
+} // of namespace simgear
+
+#endif // of SG_IO_DAVMULTISTATUS_HXX
diff --git a/simgear/io/SVNDirectory.cxx b/simgear/io/SVNDirectory.cxx
new file mode 100644 (file)
index 0000000..fa857f1
--- /dev/null
@@ -0,0 +1,368 @@
+
+#include "SVNDirectory.hxx"
+
+#include <cassert>
+#include <fstream>
+#include <iostream>
+#include <boost/foreach.hpp>
+
+#include <simgear/debug/logstream.hxx>
+#include <simgear/misc/strutils.hxx>
+#include <simgear/misc/sg_dir.hxx>
+#include <simgear/io/HTTPClient.hxx>
+#include <simgear/io/DAVMultiStatus.hxx>
+#include <simgear/io/SVNRepository.hxx>
+#include <simgear/io/sg_file.hxx>
+#include <simgear/io/SVNReportParser.hxx>
+#include <simgear/package/md5.h>
+#include <simgear/structure/exception.hxx>
+
+using std::string;
+using std::cout;
+using std::endl;
+using namespace simgear;
+
+typedef std::vector<HTTP::Request_ptr> RequestVector;
+typedef std::map<std::string, DAVResource*> DAVResourceMap;
+
+
+const char* DAV_CACHE_NAME = ".terrasync_cache";
+const char* CACHE_VERSION_4_TOKEN = "terrasync-cache-4";
+const int MAX_UPDATE_REPORT_DEPTH = 3;
+
+enum LineState
+{
+  LINESTATE_HREF = 0,
+  LINESTATE_VERSIONNAME
+};
+
+SVNDirectory::SVNDirectory(SVNRepository *r, const SGPath& path) :
+  localPath(path),
+  dav(NULL),
+  repo(r),
+  _doingUpdateReport(false),
+  _parent(NULL)
+{
+  if (path.exists()) {
+    parseCache();
+  } 
+  
+  // don't create dir here, repo might not exist at all
+}
+  
+SVNDirectory::SVNDirectory(SVNDirectory* pr, DAVCollection* col) :
+  dav(col),
+  repo(pr->repository()),
+  _doingUpdateReport(false),
+  _parent(pr)
+{
+  assert(col->container());
+  assert(!col->url().empty());
+  assert(_parent);
+
+  localPath = pr->fsDir().file(col->name());
+  if (!localPath.exists()) {
+    Dir d(localPath);
+    d.create(0755);
+    writeCache();
+  } else {
+    parseCache();
+  }
+}
+
+SVNDirectory::~SVNDirectory()
+{
+    // recursive delete our child directories
+    BOOST_FOREACH(SVNDirectory* d, _children) {
+        delete d;
+    }
+}
+
+void SVNDirectory::parseCache()
+{
+  SGPath p(localPath);
+  p.append(DAV_CACHE_NAME);
+  if (!p.exists()) {
+    return;
+  }
+    
+  char href[1024];
+  char versionName[128];
+  LineState lineState = LINESTATE_HREF;
+  std::ifstream file(p.c_str());
+  bool doneSelf = false;
+    
+  file.getline(href, 1024);
+  if (strcmp(CACHE_VERSION_4_TOKEN, href)) {
+    SG_LOG(SG_IO, SG_WARN, "invalid cache file:" << p.str());
+    return;
+  }
+    
+    std::string vccUrl;
+    file.getline(href, 1024);
+    vccUrl = href;
+    
+  while (!file.eof()) {
+    if (lineState == LINESTATE_HREF) {
+      file.getline(href, 1024);
+      ++lineState;
+    } else {
+      assert(lineState == LINESTATE_VERSIONNAME);
+      file.getline(versionName, 1024);
+      lineState = LINESTATE_HREF;
+      char* hrefPtr = href;
+    
+      if (!doneSelf) {
+        if (!dav) {
+          dav = new DAVCollection(hrefPtr);
+          dav->setVersionName(versionName);
+        } else {
+          assert(string(hrefPtr) == dav->url());
+        }
+        
+        if (!vccUrl.empty()) {
+            dav->setVersionControlledConfiguration(vccUrl);
+        }
+        
+        _cachedRevision = versionName;
+        doneSelf = true;
+      } else {
+        DAVResource* child = addChildDirectory(hrefPtr)->collection();
+        child->setVersionName(versionName);
+      }
+    } // of line-state switching 
+  } // of file get-line loop
+}
+  
+void SVNDirectory::writeCache()
+{
+  SGPath p(localPath);
+  if (!p.exists()) {
+      Dir d(localPath);
+      d.create(0755);
+  }
+  
+  p.append(DAV_CACHE_NAME);
+    
+  std::ofstream file(p.c_str(), std::ios::trunc);
+// first, cache file version header
+  file << CACHE_VERSION_4_TOKEN << '\n';
+// second, the repository VCC url
+  file << dav->versionControlledConfiguration() << '\n';
+      
+// third, our own URL, and version
+  file << dav->url() << '\n' << _cachedRevision << '\n';
+  
+  BOOST_FOREACH(DAVResource* child, dav->contents()) {
+    if (child->isCollection()) {
+        file << child->name() << '\n' << child->versionName() << "\n";
+    }
+  } // of child iteration
+}
+
+void SVNDirectory::setBaseUrl(const string& url)
+{
+    if (_parent) {
+        SG_LOG(SG_IO, SG_ALERT, "setting base URL on non-root directory " << url);
+        return;
+    }
+    
+    if (dav && (url == dav->url())) {
+        return;
+    }
+    
+    dav = new DAVCollection(url);
+}
+
+std::string SVNDirectory::url() const
+{
+  if (!_parent) {
+    return repo->baseUrl();
+  }
+  
+  return _parent->url() + "/" + name();
+}
+
+std::string SVNDirectory::name() const
+{
+    return dav->name();
+} 
+
+DAVResource*
+SVNDirectory::addChildFile(const std::string& fileName)
+{
+    DAVResource* child = NULL;
+    child = new DAVResource(dav->urlForChildWithName(fileName));
+    dav->addChild(child);
+  
+    writeCache();
+    return child;
+}
+
+SVNDirectory*
+SVNDirectory::addChildDirectory(const std::string& dirName)
+{
+    if (dav->childWithName(dirName)) {
+        // existing child, let's remove it
+        deleteChildByName(dirName);
+    }
+    
+    DAVCollection* childCol = dav->createChildCollection(dirName);
+    SVNDirectory* child = new SVNDirectory(this, childCol);
+    _children.push_back(child);
+    writeCache();
+    return child;
+}
+
+void SVNDirectory::deleteChildByName(const std::string& nm)
+{
+    DAVResource* child = dav->childWithName(nm);
+    if (!child) {
+//        std::cerr << "ZZZ: deleteChildByName: unknown:" << nm << std::endl;
+        return;
+    }
+
+    SGPath path = fsDir().file(nm);
+    dav->removeChild(child);
+    delete child;
+    
+    if (child->isCollection()) {
+        Dir d(path);
+        d.remove(true);
+    
+        DirectoryList::iterator it = findChildDir(nm);
+        if (it != _children.end()) {
+            SVNDirectory* c = *it;
+    //        std::cout << "YYY: deleting SVNDirectory for:" << nm << std::endl;
+            delete c;
+            _children.erase(it);
+        }
+    } else {
+        path.remove();
+    }
+    
+    writeCache();
+}
+  
+void SVNDirectory::requestFailed(HTTP::Request *req)
+{
+    SG_LOG(SG_IO, SG_WARN, "Request failed for:" << req->url());
+}
+  
+bool SVNDirectory::isDoingSync() const
+{
+  if (_doingUpdateReport) {
+      return true;
+  } 
+
+  BOOST_FOREACH(SVNDirectory* child, _children) {
+      if (child->isDoingSync()) {
+          return true;
+      } // of children
+  }
+    
+  return false;
+}
+
+void SVNDirectory::beginUpdateReport()
+{
+    _doingUpdateReport = true;
+    _cachedRevision.clear();
+    writeCache();
+}
+
+void SVNDirectory::updateReportComplete()
+{
+    _cachedRevision = dav->versionName();
+    _doingUpdateReport = false;
+    writeCache();
+    
+    SVNDirectory* pr = parent();
+    if (pr) {
+        pr->writeCache();
+    }    
+}
+
+SVNRepository* SVNDirectory::repository() const
+{
+    return repo;
+}
+
+void SVNDirectory::mergeUpdateReportDetails(unsigned int depth, 
+    string_list& items)
+{
+    // normal, easy case: we are fully in-sync at a revision
+    if (!_cachedRevision.empty()) {
+        std::ostringstream os;
+        os << "<S:entry rev=\"" << _cachedRevision << "\" depth=\"infinity\">"
+            << repoPath() << "</S:entry>";
+        items.push_back(os.str());
+        return;
+    }
+    
+    Dir d(localPath);
+    if (depth >= MAX_UPDATE_REPORT_DEPTH) {
+        std::cerr << localPath << "exceeded MAX_UPDATE_REPORT_DEPTH, cleaning" << std::endl;
+        d.removeChildren();
+        return;
+    }
+    
+    PathList cs = d.children(Dir::NO_DOT_OR_DOTDOT | Dir::INCLUDE_HIDDEN | Dir::TYPE_DIR);
+    BOOST_FOREACH(SGPath path, cs) {
+        SVNDirectory* c = child(path.file());
+        if (!c) {
+            // ignore this child, if it's an incomplete download,
+            // it will be over-written on the update anyway
+            //std::cerr << "unknown SVN child" << path << std::endl;
+        } else {
+            // recurse down into children
+            c->mergeUpdateReportDetails(depth+1, items);
+        }
+    } // of child dir iteration
+}
+
+std::string SVNDirectory::repoPath() const
+{
+    if (!_parent) {
+        return "/";
+    }
+    
+    // find the length of the repository base URL, then
+    // trim that off our repo URL - job done!
+    size_t baseUrlLen = repo->baseUrl().size();
+    return dav->url().substr(baseUrlLen + 1);
+}
+
+SVNDirectory* SVNDirectory::parent() const
+{
+    return _parent;
+}
+
+SVNDirectory* SVNDirectory::child(const std::string& dirName) const
+{
+    BOOST_FOREACH(SVNDirectory* d, _children) {
+        if (d->name() == dirName) {
+            return d;
+        }
+    }
+    
+    return NULL;
+}
+
+DirectoryList::iterator
+SVNDirectory::findChildDir(const std::string& dirName)
+{
+    DirectoryList::iterator it;
+    for (it=_children.begin(); it != _children.end(); ++it) {
+        if ((*it)->name() == dirName) {
+            return it;
+        }
+    }
+    return it;
+}
+
+simgear::Dir SVNDirectory::fsDir() const
+{
+    return Dir(localPath);
+}
diff --git a/simgear/io/SVNDirectory.hxx b/simgear/io/SVNDirectory.hxx
new file mode 100644 (file)
index 0000000..06876dd
--- /dev/null
@@ -0,0 +1,112 @@
+// DAVCollectionMirror.hxx - mirror a DAV collection to the local filesystem
+//
+// Copyright (C) 2013  James Turner <zakalawe@mac.com>
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License as
+// published by the Free Software Foundation; either version 2 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+
+#ifndef SG_IO_DAVCOLLECTIONMIRROR_HXX
+#define SG_IO_DAVCOLLECTIONMIRROR_HXX
+
+#include <string>
+#include <vector>
+#include <memory>
+
+#include <simgear/misc/sg_path.hxx>
+#include <simgear/misc/strutils.hxx>
+#include <simgear/io/DAVMultiStatus.hxx>
+
+namespace simgear  {
+
+class Dir;
+namespace HTTP { class Request; }
+
+// forward decls
+class DAVMirror;
+class SVNRepository;
+class SVNDirectory;
+
+typedef std::vector<SVNDirectory*> DirectoryList;
+
+class SVNDirectory
+{
+public:
+  // init from local
+  SVNDirectory(SVNRepository *repo, const SGPath& path);
+  ~SVNDirectory();
+  
+  void setBaseUrl(const std::string& url);
+  
+  // init from a collection
+  SVNDirectory(SVNDirectory* pr, DAVCollection* col);
+  
+//  void update();
+ // void gotResource(HTTP::Request* get, const std::string& etag);
+  void requestFailed(HTTP::Request* req);
+  
+  void beginUpdateReport();
+  void updateReportComplete();
+  
+  bool isDoingSync() const;
+  
+  std::string url() const;
+  
+  std::string name() const;
+      
+  DAVResource* addChildFile(const std::string& fileName);
+  SVNDirectory* addChildDirectory(const std::string& dirName);
+      
+ // void updateChild(DAVResource* child);
+  void deleteChildByName(const std::string& name);
+  
+  SGPath fsPath() const
+    { return localPath; }
+  
+  simgear::Dir fsDir() const;
+  
+  std::string repoPath() const;
+    
+  SVNRepository* repository() const;
+  DAVCollection* collection() const
+    { return dav; }
+  
+  std::string cachedRevision() const
+      { return _cachedRevision; }
+  
+  void mergeUpdateReportDetails(unsigned int depth, string_list& items);
+  
+  SVNDirectory* parent() const;
+  SVNDirectory* child(const std::string& dirName) const;
+private:
+  
+  void parseCache();
+  void writeCache();
+  
+  DirectoryList::iterator findChildDir(const std::string& dirName);
+      
+  SGPath localPath;
+  DAVCollection* dav;
+  SVNRepository* repo;
+  
+  std::string _cachedRevision;
+  bool _doingUpdateReport;
+  
+  SVNDirectory* _parent;
+  DirectoryList _children;
+};
+
+} // of namespace simgear
+
+#endif // of SG_IO_DAVCOLLECTIONMIRROR_HXX
diff --git a/simgear/io/SVNReportParser.cxx b/simgear/io/SVNReportParser.cxx
new file mode 100644 (file)
index 0000000..f00973c
--- /dev/null
@@ -0,0 +1,586 @@
+// SVNReportParser -- parser for SVN report XML data
+//
+// Copyright (C) 2012  James Turner <zakalawe@mac.com>
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License as
+// published by the Free Software Foundation; either version 2 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+#include "SVNReportParser.hxx"
+
+#include <iostream>
+#include <cstring>
+#include <cassert>
+#include <algorithm>
+#include <sstream>
+#include <fstream>
+
+#include <boost/foreach.hpp>
+
+#include "simgear/misc/sg_path.hxx"
+#include "simgear/misc/sg_dir.hxx"
+#include "simgear/debug/logstream.hxx"
+#include "simgear/xml/xmlparse.h"
+#include "simgear/xml/easyxml.hxx"
+#include "simgear/misc/strutils.hxx"
+#include "simgear/package/md5.h"
+
+#include "SVNDirectory.hxx"
+#include "SVNRepository.hxx"
+#include "DAVMultiStatus.hxx"
+
+using std::cout;
+using std::cerr;
+using std::endl;
+using std::string;
+
+using namespace simgear;
+
+#define DAV_NS "DAV::"
+#define SVN_NS "svn::"
+#define SUBVERSION_DAV_NS "http://subversion.tigris.org/xmlns/dav/"
+
+namespace {
+    
+    #define MAX_ENCODED_INT_LEN 10
+    
+    static size_t
+    decode_size(unsigned char* &p,
+                const unsigned char *end)
+    {
+      if (p + MAX_ENCODED_INT_LEN < end)
+        end = p + MAX_ENCODED_INT_LEN;
+      /* Decode bytes until we're done.  */
+      size_t result = 0;
+      
+      while (p < end) {
+          result = (result << 7) | (*p & 0x7f);
+          if (((*p++ >> 7) & 0x1) == 0) {
+              break;
+          }
+      }
+      
+      return result;
+    }
+    
+    static bool
+    try_decode_size(unsigned char* &p,
+                const unsigned char *end)
+    {
+      if (p + MAX_ENCODED_INT_LEN < end)
+        end = p + MAX_ENCODED_INT_LEN;
+
+      while (p < end) {
+          if (((*p++ >> 7) & 0x1) == 0) {
+              return true;
+          }
+      }
+      
+      return false;
+    }
+    
+//  const char* SVN_UPDATE_REPORT_TAG = SVN_NS "update-report";
+ // const char* SVN_TARGET_REVISION_TAG = SVN_NS "target-revision";
+  const char* SVN_OPEN_DIRECTORY_TAG = SVN_NS "open-directory";
+  const char* SVN_OPEN_FILE_TAG = SVN_NS "open-file";
+  const char* SVN_ADD_DIRECTORY_TAG = SVN_NS "add-directory";
+  const char* SVN_ADD_FILE_TAG = SVN_NS "add-file";
+  const char* SVN_TXDELTA_TAG = SVN_NS "txdelta";
+  const char* SVN_SET_PROP_TAG = SVN_NS "set-prop";
+  const char* SVN_DELETE_ENTRY_TAG = SVN_NS "delete-entry";
+  
+  const char* SVN_DAV_MD5_CHECKSUM = SUBVERSION_DAV_NS ":md5-checksum";
+  
+  const char* DAV_HREF_TAG = DAV_NS "href";
+  const char* DAV_CHECKED_IN_TAG = SVN_NS "checked-in";
+  
+
+  const int svn_txdelta_source = 0;
+  const int svn_txdelta_target = 1;
+  const int svn_txdelta_new = 2;
+
+  const size_t DELTA_HEADER_SIZE = 4;
+    
+  /**
+   * helper struct to decode and store the SVN delta header
+   * values
+   */
+  struct SVNDeltaWindow
+  {
+  public:
+
+      static bool isWindowComplete(unsigned char* buffer, size_t bytes)
+      {
+          unsigned char* p = buffer;
+          unsigned char* pEnd = p + bytes;
+          // if we can't decode five sizes, certainly incomplete
+          for (int i=0; i<5; i++) {
+              if (!try_decode_size(p, pEnd)) {
+                  return false;
+              }
+          }
+          
+          p = buffer;
+        // ignore these three
+          decode_size(p, pEnd);
+          decode_size(p, pEnd);
+          decode_size(p, pEnd);
+          size_t instructionLen = decode_size(p, pEnd);
+          size_t newLength = decode_size(p, pEnd);
+          size_t headerLength = p - buffer;
+          
+          return (bytes >= (instructionLen + newLength + headerLength));
+      }
+      
+     SVNDeltaWindow(unsigned char* p) :
+         headerLength(0),
+         _ptr(p)
+     {
+         sourceViewOffset = decode_size(p, p+20);
+         sourceViewLength = decode_size(p, p+20);
+         targetViewLength = decode_size(p, p+20);
+         instructionLength = decode_size(p, p+20);
+         newLength = decode_size(p, p+20);
+         
+         headerLength = p - _ptr;
+         _ptr = p;
+         
+         if (sourceViewOffset != 0) {
+             cout << "sourceViewOffset:" << sourceViewOffset << endl;
+         }
+     }
+  
+    bool apply(std::vector<char>& output, std::istream& source)
+    {
+        unsigned char* pEnd = _ptr + instructionLength;
+        unsigned char* newData = pEnd;
+        
+        while (_ptr < pEnd) {
+          int op = ((*_ptr >> 6) & 0x3);  
+          if (op >= 3) {
+              std::cerr << "weird opcode" << endl;
+              return false;
+          }
+      
+          int length = *_ptr++ & 0x3f;
+          int offset = 0;
+        
+          if (length == 0) {
+            length = decode_size(_ptr, pEnd);
+          }
+        
+          if (length == 0) {
+            std::cerr << "malformed stream, 0 length" << std::endl;
+            return false;
+          }
+      
+          // if op != new, decode another size value
+          if (op != svn_txdelta_new) {
+            offset = decode_size(_ptr, pEnd);
+          }
+
+          if (op == svn_txdelta_target) {
+            while (length > 0) {
+              output.push_back(output[offset++]);
+              --length;
+            }
+          } else if (op == svn_txdelta_new) {
+              output.insert(output.end(), newData, newData + length);
+          } else if (op == svn_txdelta_source) {
+            source.seekg(offset);
+            char* sourceBuf = (char*) malloc(length);
+            assert(sourceBuf);
+            source.read(sourceBuf, length);
+            output.insert(output.end(), sourceBuf, sourceBuf + length);
+            free(sourceBuf);
+          }
+        } // of instruction loop
+        
+        return true;
+    }  
+  
+    size_t size() const
+    {
+        return headerLength + instructionLength + newLength;
+    }
+  
+    unsigned int sourceViewOffset;
+    size_t sourceViewLength,
+      targetViewLength;
+    size_t headerLength,
+        instructionLength,
+        newLength;
+    
+private:  
+    unsigned char* _ptr;
+  };
+  
+  
+} // of anonymous namespace
+
+class SVNReportParser::SVNReportParserPrivate
+{
+public:
+  SVNReportParserPrivate(SVNRepository* repo) :
+    tree(repo),
+    status(SVNRepository::NO_ERROR),
+    parserInited(false),
+    currentPath(repo->fsBase())
+  {
+    inFile = false;
+    currentDir = repo->rootDir();
+  }
+
+  ~SVNReportParserPrivate()
+  {
+  }
+  
+  void startElement (const char * name, const char** attributes)
+  {    
+      if (status != SVNRepository::NO_ERROR) {
+          return;
+      }
+      
+    ExpatAtts attrs(attributes);
+    tagStack.push_back(name);
+    if (!strcmp(name, SVN_TXDELTA_TAG)) {
+        txDeltaData.clear();
+    } else if (!strcmp(name, SVN_ADD_FILE_TAG)) {
+      string fileName(attrs.getValue("name"));
+      SGPath filePath(currentDir->fsDir().file(fileName));
+      currentPath = filePath;
+      inFile = true;
+    } else if (!strcmp(name, SVN_OPEN_FILE_TAG)) {
+      string fileName(attrs.getValue("name"));
+      SGPath filePath(Dir(currentPath).file(fileName));
+      currentPath = filePath;
+      
+      DAVResource* res = currentDir->collection()->childWithName(fileName);   
+      if (!res || !filePath.exists()) {
+        // set error condition
+      }
+      
+      inFile = true;
+    } else if (!strcmp(name, SVN_ADD_DIRECTORY_TAG)) {
+      string dirName(attrs.getValue("name"));
+      Dir d(currentDir->fsDir().file(dirName));
+      if (d.exists()) {
+          // policy decision : if we're doing an add, wipe the existing
+          d.remove(true);
+      }
+    
+      currentDir = currentDir->addChildDirectory(dirName);
+      currentPath = currentDir->fsPath();
+      currentDir->beginUpdateReport();
+      //cout << "addDir:" << currentPath << endl;
+    } else if (!strcmp(name, SVN_SET_PROP_TAG)) {
+      setPropName = attrs.getValue("name");
+      setPropValue.clear();
+    } else if (!strcmp(name, SVN_DAV_MD5_CHECKSUM)) {
+      md5Sum.clear();
+    } else if (!strcmp(name, SVN_OPEN_DIRECTORY_TAG)) {
+        string dirName;
+        if (attrs.getValue("name")) {
+            dirName = string(attrs.getValue("name"));
+        }
+        openDirectory(dirName);
+    } else if (!strcmp(name, SVN_DELETE_ENTRY_TAG)) {
+        string entryName(attrs.getValue("name"));
+        deleteEntry(entryName);
+    } else if (!strcmp(name, DAV_CHECKED_IN_TAG) || !strcmp(name, DAV_HREF_TAG)) {
+        // don't warn on these ones
+    } else {
+        //std::cerr << "unhandled element:" << name << std::endl;
+    }
+  } // of startElement
+  
+  void openDirectory(const std::string& dirName)
+  {
+      if (dirName.empty()) {
+          // root directory, we shall assume
+          currentDir = tree->rootDir();
+      } else {
+          assert(currentDir);
+          currentDir = currentDir->child(dirName);
+      }
+      
+      assert(currentDir);
+      currentPath = currentDir->fsPath();
+      currentDir->beginUpdateReport();
+  }
+  
+  void deleteEntry(const std::string& entryName)
+  {
+      currentDir->deleteChildByName(entryName);
+  }
+  
+  bool decodeTextDelta(const SGPath& outputPath)
+  {
+    string decoded = strutils::decodeBase64(txDeltaData);
+    size_t bytesToDecode = decoded.size();
+    std::vector<char> output;      
+    unsigned char* p = (unsigned char*) decoded.data();
+    if (memcmp(decoded.data(), "SVN\0", DELTA_HEADER_SIZE) != 0) {
+        return false; // bad header
+    }
+    
+    bytesToDecode -= DELTA_HEADER_SIZE;
+    p += DELTA_HEADER_SIZE;
+    std::ifstream source;
+    source.open(outputPath.c_str(), std::ios::in | std::ios::binary);
+    
+    while (bytesToDecode > 0) {  
+        SVNDeltaWindow window(p);      
+        assert(bytesToDecode >= window.size());
+        window.apply(output, source);
+        bytesToDecode -= window.size();
+        p += window.size();
+    }
+
+    source.close();
+    std::ofstream f;
+    f.open(outputPath.c_str(), 
+      std::ios::out | std::ios::trunc | std::ios::binary);
+    f.write(output.data(), output.size());
+    
+    // compute MD5 while we have the file in memory
+    MD5_CTX md5;
+    memset(&md5, 0, sizeof(MD5_CTX));
+    MD5Init(&md5);
+    MD5Update(&md5, (unsigned char*) output.data(), output.size());
+    MD5Final(&md5);
+    decodedFileMd5 = strutils::encodeHex(md5.digest, 16);
+    return true;
+  }
+  
+  void endElement (const char * name)
+  {
+      if (status != SVNRepository::NO_ERROR) {
+          return;
+      }
+      
+    assert(tagStack.back() == name);
+    tagStack.pop_back();
+    if (!strcmp(name, SVN_TXDELTA_TAG)) {
+      if (!decodeTextDelta(currentPath)) {
+          fail(SVNRepository::ERROR_TXDELTA);
+      }
+    } else if (!strcmp(name, SVN_ADD_FILE_TAG)) {
+      finishFile(currentDir->addChildFile(currentPath.file()));
+    } else if (!strcmp(name, SVN_OPEN_FILE_TAG)) {
+      DAVResource* res = currentDir->collection()->childWithName(currentPath.file());   
+      assert(res);
+      finishFile(res);
+    } else if (!strcmp(name, SVN_ADD_DIRECTORY_TAG)) {
+      // pop directory
+      currentPath = currentPath.dir();
+      currentDir->updateReportComplete();
+      currentDir = currentDir->parent();
+    } else if (!strcmp(name, SVN_SET_PROP_TAG)) {
+      if (setPropName == "svn:entry:committed-rev") {
+        revision = strutils::to_int(setPropValue);
+        currentVersionName = setPropValue;
+        if (!inFile) {
+          // for directories we have the resource already
+          // for adding files, we might not; we set the version name
+          // above when ending the add/open-file element
+          currentDir->collection()->setVersionName(currentVersionName);
+        } 
+      }
+    } else if (!strcmp(name, SVN_DAV_MD5_CHECKSUM)) {
+      // validate against (presumably) just written file
+      if (decodedFileMd5 != md5Sum) {
+        fail(SVNRepository::ERROR_CHECKSUM);
+      }
+    } else if (!strcmp(name, SVN_OPEN_DIRECTORY_TAG)) {
+        if (currentDir->parent()) {   
+          // pop the collection stack
+          currentDir = currentDir->parent();
+        }
+        
+        currentDir->updateReportComplete();
+        currentPath = currentDir->fsPath();
+    } else {
+    //  std::cout << "element:" << name;
+    }
+  }
+  
+  void finishFile(DAVResource* res)
+  {
+      res->setVersionName(currentVersionName);
+      res->setMD5(md5Sum);
+      currentPath = currentPath.dir();
+      inFile = false;
+  }
+  
+  void data (const char * s, int length)
+  {
+      if (status != SVNRepository::NO_ERROR) {
+          return;
+      }
+      
+    if (tagStack.back() == SVN_SET_PROP_TAG) {
+      setPropValue += string(s, length);
+    } else if (tagStack.back() == SVN_TXDELTA_TAG) {
+      txDeltaData += string(s, length);
+    } else if (tagStack.back() == SVN_DAV_MD5_CHECKSUM) {
+      md5Sum += string(s, length);
+    }
+  }
+  
+  void pi (const char * target, const char * data) {}
+  
+  string tagN(const unsigned int n) const
+  {
+    int sz = tagStack.size();
+    if (n >= sz) {
+      return string();
+    }
+    
+    return tagStack[sz - (1 + n)];
+  }
+  
+  void fail(SVNRepository::ResultCode err)
+  {
+      status = err;
+  }
+  
+  SVNRepository* tree;
+  DAVCollection* rootCollection;
+  SVNDirectory* currentDir;
+  SVNRepository::ResultCode status;
+  
+  bool parserInited;
+  XML_Parser xmlParser;
+  
+// in-flight data
+  string_list tagStack;
+  string currentVersionName;
+  string txDeltaData;
+  SGPath currentPath;
+  bool inFile;
+    
+  unsigned int revision;
+  string md5Sum, decodedFileMd5;
+  std::string setPropName, setPropValue;
+};
+
+
+////////////////////////////////////////////////////////////////////////
+// Static callback functions for Expat.
+////////////////////////////////////////////////////////////////////////
+
+#define VISITOR static_cast<SVNReportParser::SVNReportParserPrivate *>(userData)
+
+static void
+start_element (void * userData, const char * name, const char ** atts)
+{
+  VISITOR->startElement(name, atts);
+}
+
+static void
+end_element (void * userData, const char * name)
+{
+  VISITOR->endElement(name);
+}
+
+static void
+character_data (void * userData, const char * s, int len)
+{
+  VISITOR->data(s, len);
+}
+
+static void
+processing_instruction (void * userData,
+                        const char * target,
+                        const char * data)
+{
+  VISITOR->pi(target, data);
+}
+
+#undef VISITOR
+
+///////////////////////////////////////////////////////////////////////////////
+
+SVNReportParser::SVNReportParser(SVNRepository* repo) :
+  _d(new SVNReportParserPrivate(repo))
+{
+  
+}
+
+SVNReportParser::~SVNReportParser()
+{
+}
+
+SVNRepository::ResultCode
+SVNReportParser::innerParseXML(const char* data, int size)
+{
+    if (_d->status != SVNRepository::NO_ERROR) {
+        return _d->status;
+    }
+    
+    bool isEnd = (data == NULL);
+    if (!XML_Parse(_d->xmlParser, data, size, isEnd)) {
+      SG_LOG(SG_IO, SG_INFO, "SVN parse error:" << XML_ErrorString(XML_GetErrorCode(_d->xmlParser))
+             << " at line:" << XML_GetCurrentLineNumber(_d->xmlParser)
+             << " column " << XML_GetCurrentColumnNumber(_d->xmlParser));
+    
+      XML_ParserFree(_d->xmlParser);
+      _d->parserInited = false;
+      return SVNRepository::ERROR_XML;
+    } else if (isEnd) {
+        XML_ParserFree(_d->xmlParser);
+        _d->parserInited = false;
+    }
+    
+    return _d->status;
+}
+
+SVNRepository::ResultCode
+SVNReportParser::parseXML(const char* data, int size)
+{
+    if (_d->status != SVNRepository::NO_ERROR) {
+        return _d->status;
+    }
+    
+  if (!_d->parserInited) {
+    _d->xmlParser = XML_ParserCreateNS(0, ':');
+    XML_SetUserData(_d->xmlParser, _d.get());
+    XML_SetElementHandler(_d->xmlParser, start_element, end_element);
+    XML_SetCharacterDataHandler(_d->xmlParser, character_data);
+    XML_SetProcessingInstructionHandler(_d->xmlParser, processing_instruction);
+    _d->parserInited = true;
+  }
+  
+  return innerParseXML(data, size);
+}
+
+SVNRepository::ResultCode SVNReportParser::finishParse()
+{
+    if (_d->status != SVNRepository::NO_ERROR) {
+        return _d->status;
+    }
+    
+    return innerParseXML(NULL, 0);
+}
+
+std::string SVNReportParser::etagFromRevision(unsigned int revision)
+{
+  // etags look like W/"7//", hopefully this is stable
+  // across different servers and similar
+  std::ostringstream os;
+  os << "W/\"" << revision << "//";
+  return os.str();
+}
+
+
diff --git a/simgear/io/SVNReportParser.hxx b/simgear/io/SVNReportParser.hxx
new file mode 100644 (file)
index 0000000..1e94751
--- /dev/null
@@ -0,0 +1,57 @@
+// SVNReportParser -- parser for SVN report XML data
+//
+// Copyright (C) 2012  James Turner <zakalawe@mac.com>
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License as
+// published by the Free Software Foundation; either version 2 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+
+#ifndef SG_IO_SVNREPORTPARSER_HXX
+#define SG_IO_SVNREPORTPARSER_HXX
+
+#include <string>
+#include <memory> // for auto_ptr
+
+#include "SVNRepository.hxx"
+
+class SGPath;
+
+namespace simgear
+{
+  
+class SVNRepository;
+
+class SVNReportParser
+{
+public:
+  SVNReportParser(SVNRepository* repo);
+  ~SVNReportParser();
+  
+  // incremental XML parsing
+  SVNRepository::ResultCode parseXML(const char* data, int size);
+  
+  SVNRepository::ResultCode finishParse();
+        
+  static std::string etagFromRevision(unsigned int revision);
+          
+  class SVNReportParserPrivate;
+private:
+    SVNRepository::ResultCode innerParseXML(const char* data, int size);
+        
+  std::auto_ptr<SVNReportParserPrivate> _d;
+};
+
+} // of namespace simgear
+
+#endif // of SG_IO_SVNREPORTPARSER_HXX
diff --git a/simgear/io/SVNRepository.cxx b/simgear/io/SVNRepository.cxx
new file mode 100644 (file)
index 0000000..b8768ea
--- /dev/null
@@ -0,0 +1,414 @@
+// DAVMirrorTree -- mirror a DAV tree to the local file-system
+//
+// Copyright (C) 2012  James Turner <zakalawe@mac.com>
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License as
+// published by the Free Software Foundation; either version 2 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+#include "SVNRepository.hxx"
+
+#include <iostream>
+#include <cstring>
+#include <cassert>
+#include <algorithm>
+#include <sstream>
+#include <map>
+#include <set>
+#include <fstream>
+
+#include <boost/foreach.hpp>
+
+#include "simgear/debug/logstream.hxx"
+#include "simgear/misc/strutils.hxx"
+#include <simgear/misc/sg_dir.hxx>
+#include <simgear/io/HTTPClient.hxx>
+#include <simgear/io/DAVMultiStatus.hxx>
+#include <simgear/io/SVNDirectory.hxx>
+#include <simgear/io/sg_file.hxx>
+#include <simgear/io/SVNReportParser.hxx>
+
+using std::cout;
+using std::cerr;
+using std::endl;
+using std::string;
+
+namespace simgear
+{
+
+typedef std::vector<HTTP::Request_ptr> RequestVector;
+
+class SVNRepoPrivate
+{
+public:
+    SVNRepoPrivate(SVNRepository* parent) : 
+        p(parent), 
+        isUpdating(false),
+        status(SVNRepository::NO_ERROR)
+    { ; }
+    
+    SVNRepository* p; // link back to outer
+    SVNDirectory* rootCollection;
+    HTTP::Client* http;
+    std::string baseUrl;
+    std::string vccUrl;
+    std::string targetRevision;
+    bool isUpdating;
+    SVNRepository::ResultCode status;
+    
+    void svnUpdateDone()
+    {
+        isUpdating = false;
+    }
+    
+    void updateFailed(HTTP::Request* req, SVNRepository::ResultCode err)
+    {
+        SG_LOG(SG_IO, SG_WARN, "SVN: failed to update from:" << req->url());
+        isUpdating = false;
+        status = err;
+    }
+      
+    void propFindComplete(HTTP::Request* req, DAVCollection* col);
+    void propFindFailed(HTTP::Request* req, SVNRepository::ResultCode err);
+};
+
+
+namespace { // anonmouse
+    
+    string makeAbsoluteUrl(const string& url, const string& base)
+    {
+      if (strutils::starts_with(url, "http://"))
+        return url; // already absolute
+  
+      assert(strutils::starts_with(base, "http://"));
+      int schemeEnd = base.find("://");
+      int hostEnd = base.find('/', schemeEnd + 3);
+      if (hostEnd < 0) {
+        return url;
+      }
+  
+      return base.substr(0, hostEnd) + url;
+    }
+    
+    // keep the responses small by only requesting the properties we actually
+    // care about; the ETag, length and MD5-sum
+    const char* PROPFIND_REQUEST_BODY =
+      "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
+      "<D:propfind xmlns:D=\"DAV:\">"
+      "<D:prop xmlns:R=\"http://subversion.tigris.org/xmlns/dav/\">"
+      "<D:resourcetype/>"
+      "<D:version-name/>"
+      "<D:version-controlled-configuration/>"
+      "</D:prop>"
+      "</D:propfind>";
+
+    class PropFindRequest : public HTTP::Request
+    {
+    public:
+      PropFindRequest(SVNRepoPrivate* repo) :
+        Request(repo->baseUrl, "PROPFIND"),
+        _repo(repo)
+      {
+      }
+  
+      virtual string_list requestHeaders() const
+      {
+        string_list r;
+        r.push_back("Depth");
+        return r;
+      }
+  
+      virtual string header(const string& name) const
+      {
+          if (name == "Depth") {
+              return "0";
+          }
+          
+          return string();
+      }
+  
+      virtual string requestBodyType() const
+      {
+          return "application/xml; charset=\"utf-8\"";
+      }
+  
+      virtual int requestBodyLength() const
+      {
+        return strlen(PROPFIND_REQUEST_BODY);
+      }
+  
+      virtual int getBodyData(char* buf, int count) const
+      {
+        int bodyLen = strlen(PROPFIND_REQUEST_BODY);
+        assert(count >= bodyLen);
+        memcpy(buf, PROPFIND_REQUEST_BODY, bodyLen);
+        return bodyLen;
+      }
+
+    protected:
+      virtual void responseHeadersComplete()
+      {
+        if (responseCode() == 207) {
+            // fine
+        } else if (responseCode() == 404) {
+            _repo->propFindFailed(this, SVNRepository::ERROR_NOT_FOUND);
+        } else {
+            SG_LOG(SG_IO, SG_WARN, "request for:" << url() << 
+                " return code " << responseCode());
+            _repo->propFindFailed(this, SVNRepository::ERROR_SOCKET);
+        }
+      }
+  
+      virtual void responseComplete()
+      {
+        if (responseCode() == 207) {
+          _davStatus.finishParse();
+          _repo->propFindComplete(this, (DAVCollection*) _davStatus.resource());
+        }
+      }
+  
+      virtual void gotBodyData(const char* s, int n)
+      {
+        if (responseCode() != 207) {
+          return;
+        }
+        _davStatus.parseXML(s, n);
+      }
+    private:
+      SVNRepoPrivate* _repo;
+      DAVMultiStatus _davStatus;
+    };
+
+class UpdateReportRequest : public HTTP::Request
+{
+public:
+  UpdateReportRequest(SVNRepoPrivate* repo, 
+      const std::string& aVersionName,
+      bool startEmpty) :
+    HTTP::Request("", "REPORT"),
+    _requestSent(0),
+    _parser(repo->p),
+    _repo(repo),
+    _failed(false)
+  {       
+    setUrl(repo->vccUrl);
+
+    _request =
+    "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"
+    "<S:update-report send-all=\"true\" xmlns:S=\"svn:\">\n"
+    "<S:src-path>" + repo->baseUrl + "</S:src-path>\n"
+    "<S:depth>unknown</S:depth>\n";
+
+    _request += "<S:entry rev=\"" + aVersionName + "\" depth=\"infinity\" start-empty=\"true\"/>\n";
+     
+    if (!startEmpty) {
+        string_list entries;
+        _repo->rootCollection->mergeUpdateReportDetails(0, entries);
+        BOOST_FOREACH(string e, entries) {
+            _request += e + "\n";
+        }
+    }
+
+    _request += "</S:update-report>";   
+  }
+
+  virtual string requestBodyType() const
+  {
+    return "application/xml; charset=\"utf-8\"";
+  }
+
+  virtual int requestBodyLength() const
+  {
+    return _request.size();
+  }
+
+  virtual int getBodyData(char* buf, int count) const
+  {
+    int len = std::min(count, requestBodyLength() - _requestSent);
+    memcpy(buf, _request.c_str() + _requestSent, len);
+    _requestSent += len;
+    return len;
+  }
+
+protected:
+  virtual void responseHeadersComplete()
+  {
+
+  }
+
+  virtual void responseComplete()
+  {
+      if (_failed) {
+          return;
+      }
+      
+    if (responseCode() == 200) {
+          SVNRepository::ResultCode err = _parser.finishParse();
+          if (err) {
+              _repo->updateFailed(this, err);
+              _failed = true;
+          } else {
+              _repo->svnUpdateDone();
+          }
+    } else if (responseCode() == 404) {
+        _repo->updateFailed(this, SVNRepository::ERROR_NOT_FOUND);
+        _failed = true;
+    } else {
+        SG_LOG(SG_IO, SG_WARN, "SVN: request for:" << url() <<
+        " return code " << responseCode());
+        _repo->updateFailed(this, SVNRepository::ERROR_SOCKET);
+        _failed = true;
+    }
+  }
+
+  virtual void gotBodyData(const char* s, int n)
+  {    
+      if (_failed) {
+          return;
+      }
+      
+    if (responseCode() != 200) {
+        return;
+    }
+    
+    //cout << "body data:" << string(s, n) << endl;
+    SVNRepository::ResultCode err = _parser.parseXML(s, n);
+    if (err) {
+        _failed = true;
+        SG_LOG(SG_IO, SG_WARN, "SVN: request for:" << url() <<
+            " XML parse failed");
+        _repo->updateFailed(this, err);
+    }
+  }
+
+
+private:
+  string _request;
+  mutable int _requestSent;
+  SVNReportParser _parser;
+  SVNRepoPrivate* _repo;
+  bool _failed;
+};
+        
+} // anonymous 
+
+SVNRepository::SVNRepository(const SGPath& base, HTTP::Client *cl) :
+  _d(new SVNRepoPrivate(this))
+{
+  _d->http = cl;
+  _d->rootCollection = new SVNDirectory(this, base);
+  _d->baseUrl = _d->rootCollection->url();  
+}
+
+SVNRepository::~SVNRepository()
+{
+    delete _d->rootCollection;
+}
+
+void SVNRepository::setBaseUrl(const std::string &url)
+{
+  _d->baseUrl = url;
+  _d->rootCollection->setBaseUrl(url);
+}
+
+std::string SVNRepository::baseUrl() const
+{
+  return _d->baseUrl;
+}
+
+HTTP::Client* SVNRepository::http() const
+{
+  return _d->http;
+}
+
+SGPath SVNRepository::fsBase() const
+{
+  return _d->rootCollection->fsPath();
+}
+
+bool SVNRepository::isBare() const
+{
+    if (!fsBase().exists() || Dir(fsBase()).isEmpty()) {
+        return true;
+    }
+    
+    if (_d->vccUrl.empty()) {
+        return true;
+    }
+    
+    return false;
+}
+
+void SVNRepository::update()
+{  
+    _d->status = NO_ERROR;
+    if (_d->targetRevision.empty() || _d->vccUrl.empty()) {        
+        _d->isUpdating = true;        
+        PropFindRequest* pfr = new PropFindRequest(_d.get());
+        http()->makeRequest(pfr);
+        return;
+    }
+        
+    if (_d->targetRevision == rootDir()->cachedRevision()) {
+        SG_LOG(SG_IO, SG_DEBUG, baseUrl() << " in sync at version " << _d->targetRevision);
+        _d->isUpdating = false;
+        return;
+    }
+    
+    _d->isUpdating = true;
+    UpdateReportRequest* urr = new UpdateReportRequest(_d.get(), 
+        _d->targetRevision, isBare());
+    http()->makeRequest(urr);
+}
+  
+bool SVNRepository::isDoingSync() const
+{
+    if (_d->status != NO_ERROR) {
+        return false;
+    }
+    
+    return _d->isUpdating || _d->rootCollection->isDoingSync();
+}
+
+SVNDirectory* SVNRepository::rootDir() const
+{
+    return _d->rootCollection;
+}
+
+SVNRepository::ResultCode
+SVNRepository::failure() const
+{
+    return _d->status;
+}
+
+///////////////////////////////////////////////////////////////////////////
+
+void SVNRepoPrivate::propFindComplete(HTTP::Request* req, DAVCollection* c)
+{
+    targetRevision = c->versionName();
+    vccUrl = makeAbsoluteUrl(c->versionControlledConfiguration(), baseUrl);
+    rootCollection->collection()->setVersionControlledConfiguration(vccUrl);    
+    p->update();
+}
+  
+void SVNRepoPrivate::propFindFailed(HTTP::Request *req, SVNRepository::ResultCode err)
+{
+    if (err != SVNRepository::ERROR_NOT_FOUND) {
+        SG_LOG(SG_IO, SG_WARN, "PropFind failed for:" << req->url());
+    }
+    
+    isUpdating = false;
+    status = err;
+}
+
+} // of namespace simgear
diff --git a/simgear/io/SVNRepository.hxx b/simgear/io/SVNRepository.hxx
new file mode 100644 (file)
index 0000000..535990c
--- /dev/null
@@ -0,0 +1,76 @@
+// DAVMirrorTree.hxx - mirror a DAV tree to the local file system
+//
+// Copyright (C) 2012  James Turner <zakalawe@mac.com>
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License as
+// published by the Free Software Foundation; either version 2 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+
+#ifndef SG_IO_DAVMIRRORTREE_HXX
+#define SG_IO_DAVMIRRORTREE_HXX
+
+#include <string>
+#include <vector>
+#include <memory>
+
+#include <simgear/misc/sg_path.hxx>
+
+namespace simgear  {
+  
+  namespace HTTP {
+    class Client;
+  }
+  
+class SVNDirectory;  
+class SVNRepoPrivate;
+
+class SVNRepository
+{
+public:
+  
+    SVNRepository(const SGPath& root, HTTP::Client* cl);
+    ~SVNRepository();
+
+    SVNDirectory* rootDir() const;
+    SGPath fsBase() const;
+
+    void setBaseUrl(const std::string& url);
+    std::string baseUrl() const;
+
+    HTTP::Client* http() const;
+
+    void update();
+
+    bool isDoingSync() const;
+    
+    enum ResultCode {
+        NO_ERROR = 0,
+        ERROR_NOT_FOUND,
+        ERROR_SOCKET,
+        ERROR_XML,
+        ERROR_TXDELTA,
+        ERROR_IO,
+        ERROR_CHECKSUM
+    };
+    
+    ResultCode failure() const;
+private:
+    bool isBare() const;
+    
+    std::auto_ptr<SVNRepoPrivate> _d;
+};
+
+} // of namespace simgear
+
+#endif // of SG_IO_DAVMIRRORTREE_HXX
diff --git a/simgear/io/http_svn.cxx b/simgear/io/http_svn.cxx
new file mode 100644 (file)
index 0000000..a56630e
--- /dev/null
@@ -0,0 +1,49 @@
+#include <cstdio>
+#include <cstring>
+#include <signal.h>
+
+#include <iostream>
+#include <boost/foreach.hpp>
+
+
+#include <simgear/io/sg_file.hxx>
+#include <simgear/io/HTTPClient.hxx>
+#include <simgear/io/HTTPRequest.hxx>
+#include <simgear/io/sg_netChannel.hxx>
+#include <simgear/io/DAVMultiStatus.hxx>
+#include <simgear/io/SVNRepository.hxx>
+#include <simgear/debug/logstream.hxx>
+
+#include <simgear/misc/strutils.hxx>
+#include <simgear/timing/timestamp.hxx>
+
+using namespace simgear;
+using std::cout;
+using std::endl;
+using std::cerr;
+using std::string;
+
+HTTP::Client* httpClient;
+
+int main(int argc, char* argv[])
+{
+  sglog().setLogLevels( SG_ALL, SG_INFO );
+  HTTP::Client cl;
+  httpClient = &cl;
+
+  
+  SGPath p("/Users/jmt/Desktop/scenemodels");
+  SVNRepository airports(p, &cl);
+ //  airports.setBaseUrl("http://svn.goneabitbursar.com/testproject1");
+  airports.setBaseUrl("http://terrascenery.googlecode.com/svn/trunk/data/Scenery/Models");
+  
+//  airports.setBaseUrl("http://terrascenery.googlecode.com/svn/trunk/data/Scenery/Airports");
+  airports.update();
+  
+  while (airports.isDoingSync()) {
+    cl.update(100);
+  }
+  
+  cout << "all done!" << endl;
+  return EXIT_SUCCESS;
+}
\ No newline at end of file