sg_socket_udp.hxx
HTTPClient.hxx
HTTPRequest.hxx
+ DAVMultiStatus.hxx
+ SVNRepository.hxx
+ SVNDirectory.hxx
+ SVNReportParser.hxx
)
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})
--- /dev/null
+// 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;
+}
+
+
--- /dev/null
+// 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
--- /dev/null
+
+#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);
+}
--- /dev/null
+// 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
--- /dev/null
+// 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();
+}
+
+
--- /dev/null
+// 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
--- /dev/null
+// 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
--- /dev/null
+// 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
--- /dev/null
+#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