1 // SVNReportParser -- parser for SVN report XML data
3 // Copyright (C) 2012 James Turner <zakalawe@mac.com>
5 // This program is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU General Public License as
7 // published by the Free Software Foundation; either version 2 of the
8 // License, or (at your option) any later version.
10 // This program is distributed in the hope that it will be useful, but
11 // WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with this program; if not, write to the Free Software
17 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 # include <simgear_config.h>
23 #include "SVNReportParser.hxx"
32 #include <boost/foreach.hpp>
34 #include "simgear/misc/sg_path.hxx"
35 #include "simgear/misc/sg_dir.hxx"
36 #include "simgear/debug/logstream.hxx"
37 #include "simgear/xml/easyxml.hxx"
38 #include "simgear/misc/strutils.hxx"
39 #include "simgear/package/md5.h"
44 # include "sg_expat.h"
47 #include "SVNDirectory.hxx"
48 #include "SVNRepository.hxx"
49 #include "DAVMultiStatus.hxx"
56 using namespace simgear;
58 #define DAV_NS "DAV::"
59 #define SVN_NS "svn::"
60 #define SUBVERSION_DAV_NS "http://subversion.tigris.org/xmlns/dav/"
64 #define MAX_ENCODED_INT_LEN 10
67 decode_size(unsigned char* &p,
68 const unsigned char *end)
70 if (p + MAX_ENCODED_INT_LEN < end)
71 end = p + MAX_ENCODED_INT_LEN;
72 /* Decode bytes until we're done. */
76 result = (result << 7) | (*p & 0x7f);
77 if (((*p++ >> 7) & 0x1) == 0) {
86 try_decode_size(unsigned char* &p,
87 const unsigned char *end)
89 if (p + MAX_ENCODED_INT_LEN < end)
90 end = p + MAX_ENCODED_INT_LEN;
93 if (((*p++ >> 7) & 0x1) == 0) {
101 // const char* SVN_UPDATE_REPORT_TAG = SVN_NS "update-report";
102 // const char* SVN_TARGET_REVISION_TAG = SVN_NS "target-revision";
103 const char* SVN_OPEN_DIRECTORY_TAG = SVN_NS "open-directory";
104 const char* SVN_OPEN_FILE_TAG = SVN_NS "open-file";
105 const char* SVN_ADD_DIRECTORY_TAG = SVN_NS "add-directory";
106 const char* SVN_ADD_FILE_TAG = SVN_NS "add-file";
107 const char* SVN_TXDELTA_TAG = SVN_NS "txdelta";
108 const char* SVN_SET_PROP_TAG = SVN_NS "set-prop";
109 const char* SVN_DELETE_ENTRY_TAG = SVN_NS "delete-entry";
111 const char* SVN_DAV_MD5_CHECKSUM = SUBVERSION_DAV_NS ":md5-checksum";
113 const char* DAV_HREF_TAG = DAV_NS "href";
114 const char* DAV_CHECKED_IN_TAG = SVN_NS "checked-in";
117 const int svn_txdelta_source = 0;
118 const int svn_txdelta_target = 1;
119 const int svn_txdelta_new = 2;
121 const size_t DELTA_HEADER_SIZE = 4;
124 * helper struct to decode and store the SVN delta header
127 struct SVNDeltaWindow
131 static bool isWindowComplete(unsigned char* buffer, size_t bytes)
133 unsigned char* p = buffer;
134 unsigned char* pEnd = p + bytes;
135 // if we can't decode five sizes, certainly incomplete
136 for (int i=0; i<5; i++) {
137 if (!try_decode_size(p, pEnd)) {
143 // ignore these three
144 decode_size(p, pEnd);
145 decode_size(p, pEnd);
146 decode_size(p, pEnd);
147 size_t instructionLen = decode_size(p, pEnd);
148 size_t newLength = decode_size(p, pEnd);
149 size_t headerLength = p - buffer;
151 return (bytes >= (instructionLen + newLength + headerLength));
154 SVNDeltaWindow(unsigned char* p) :
158 sourceViewOffset = decode_size(p, p+20);
159 sourceViewLength = decode_size(p, p+20);
160 targetViewLength = decode_size(p, p+20);
161 instructionLength = decode_size(p, p+20);
162 newLength = decode_size(p, p+20);
164 headerLength = p - _ptr;
168 bool apply(std::vector<unsigned char>& output, std::istream& source)
170 unsigned char* pEnd = _ptr + instructionLength;
171 unsigned char* newData = pEnd;
173 while (_ptr < pEnd) {
174 int op = ((*_ptr >> 6) & 0x3);
176 SG_LOG(SG_IO, SG_INFO, "SVNDeltaWindow: bad opcode:" << op);
180 int length = *_ptr++ & 0x3f;
184 length = decode_size(_ptr, pEnd);
188 SG_LOG(SG_IO, SG_INFO, "SVNDeltaWindow: malformed stream, 0 length" << op);
192 // if op != new, decode another size value
193 if (op != svn_txdelta_new) {
194 offset = decode_size(_ptr, pEnd);
197 if (op == svn_txdelta_target) {
199 output.push_back(output[offset++]);
202 } else if (op == svn_txdelta_new) {
203 output.insert(output.end(), newData, newData + length);
204 } else if (op == svn_txdelta_source) {
205 source.seekg(offset);
206 char* sourceBuf = (char*) malloc(length);
208 source.read(sourceBuf, length);
209 output.insert(output.end(), sourceBuf, sourceBuf + length);
212 } // of instruction loop
219 return headerLength + instructionLength + newLength;
222 unsigned int sourceViewOffset;
223 size_t sourceViewLength,
234 } // of anonymous namespace
236 class SVNReportParser::SVNReportParserPrivate
239 SVNReportParserPrivate(SVNRepository* repo) :
241 status(SVNRepository::SVN_NO_ERROR),
243 currentPath(repo->fsBase())
246 currentDir = repo->rootDir();
249 ~SVNReportParserPrivate()
253 void startElement (const char * name, const char** attributes)
255 if (status != SVNRepository::SVN_NO_ERROR) {
259 ExpatAtts attrs(attributes);
260 tagStack.push_back(name);
261 if (!strcmp(name, SVN_TXDELTA_TAG)) {
263 } else if (!strcmp(name, SVN_ADD_FILE_TAG)) {
264 string fileName(attrs.getValue("name"));
265 SGPath filePath(currentDir->fsDir().file(fileName));
266 currentPath = filePath;
268 } else if (!strcmp(name, SVN_OPEN_FILE_TAG)) {
269 string fileName(attrs.getValue("name"));
270 SGPath filePath(Dir(currentPath).file(fileName));
271 currentPath = filePath;
273 DAVResource* res = currentDir->collection()->childWithName(fileName);
274 if (!res || !filePath.exists()) {
275 // set error condition
279 } else if (!strcmp(name, SVN_ADD_DIRECTORY_TAG)) {
280 string dirName(attrs.getValue("name"));
281 Dir d(currentDir->fsDir().file(dirName));
283 // policy decision : if we're doing an add, wipe the existing
287 currentDir = currentDir->addChildDirectory(dirName);
288 currentPath = currentDir->fsPath();
289 currentDir->beginUpdateReport();
290 //cout << "addDir:" << currentPath << endl;
291 } else if (!strcmp(name, SVN_SET_PROP_TAG)) {
292 setPropName = attrs.getValue("name");
293 setPropValue.clear();
294 } else if (!strcmp(name, SVN_DAV_MD5_CHECKSUM)) {
296 } else if (!strcmp(name, SVN_OPEN_DIRECTORY_TAG)) {
298 if (attrs.getValue("name")) {
299 dirName = string(attrs.getValue("name"));
301 openDirectory(dirName);
302 } else if (!strcmp(name, SVN_DELETE_ENTRY_TAG)) {
303 string entryName(attrs.getValue("name"));
304 deleteEntry(entryName);
305 } else if (!strcmp(name, DAV_CHECKED_IN_TAG) || !strcmp(name, DAV_HREF_TAG)) {
306 // don't warn on these ones
308 //std::cerr << "unhandled element:" << name << std::endl;
312 void openDirectory(const std::string& dirName)
314 if (dirName.empty()) {
315 // root directory, we shall assume
316 currentDir = tree->rootDir();
319 currentDir = currentDir->child(dirName);
323 currentPath = currentDir->fsPath();
324 currentDir->beginUpdateReport();
327 void deleteEntry(const std::string& entryName)
329 currentDir->deleteChildByName(entryName);
332 bool decodeTextDelta(const SGPath& outputPath)
334 std::vector<unsigned char> output, decoded;
335 strutils::decodeBase64(txDeltaData, decoded);
336 size_t bytesToDecode = decoded.size();
338 unsigned char* p = decoded.data();
339 if (memcmp(p, "SVN\0", DELTA_HEADER_SIZE) != 0) {
340 return false; // bad header
343 bytesToDecode -= DELTA_HEADER_SIZE;
344 p += DELTA_HEADER_SIZE;
345 std::ifstream source;
346 source.open(outputPath.c_str(), std::ios::in | std::ios::binary);
348 while (bytesToDecode > 0) {
349 if (!SVNDeltaWindow::isWindowComplete(p, bytesToDecode)) {
350 SG_LOG(SG_IO, SG_WARN, "SVN txdelta broken window");
354 SVNDeltaWindow window(p);
355 assert(bytesToDecode >= window.size());
356 window.apply(output, source);
357 bytesToDecode -= window.size();
364 f.open(outputPath.c_str(),
365 std::ios::out | std::ios::trunc | std::ios::binary);
366 f.write((char*) output.data(), output.size());
368 // compute MD5 while we have the file in memory
369 memset(&md5Context, 0, sizeof(SG_MD5_CTX));
370 SG_MD5Init(&md5Context);
371 SG_MD5Update(&md5Context, (unsigned char*) output.data(), output.size());
372 SG_MD5Final(&md5Context);
373 decodedFileMd5 = strutils::encodeHex(md5Context.digest, 16);
378 void endElement (const char * name)
380 if (status != SVNRepository::SVN_NO_ERROR) {
384 assert(tagStack.back() == name);
387 if (!strcmp(name, SVN_TXDELTA_TAG)) {
388 if (!decodeTextDelta(currentPath)) {
389 fail(SVNRepository::SVN_ERROR_TXDELTA);
391 } else if (!strcmp(name, SVN_ADD_FILE_TAG)) {
392 finishFile(currentDir->addChildFile(currentPath.file()));
393 } else if (!strcmp(name, SVN_OPEN_FILE_TAG)) {
394 DAVResource* res = currentDir->collection()->childWithName(currentPath.file());
396 SG_LOG(SG_IO, SG_WARN, "SVN open-file: unknown: " << currentPath);
397 fail(SVNRepository::SVN_ERROR_IO);
401 } else if (!strcmp(name, SVN_ADD_DIRECTORY_TAG)) {
403 currentPath = currentPath.dir();
404 currentDir->updateReportComplete();
405 currentDir = currentDir->parent();
406 } else if (!strcmp(name, SVN_SET_PROP_TAG)) {
407 if (setPropName == "svn:entry:committed-rev") {
408 revision = strutils::to_int(setPropValue);
409 currentVersionName = setPropValue;
411 // for directories we have the resource already
412 // for adding files, we might not; we set the version name
413 // above when ending the add/open-file element
414 currentDir->collection()->setVersionName(currentVersionName);
417 } else if (!strcmp(name, SVN_DAV_MD5_CHECKSUM)) {
418 // validate against (presumably) just written file
419 if (decodedFileMd5 != md5Sum) {
420 SG_LOG(SG_GENERAL, SG_INFO, "checksum fail on:" << currentPath);
421 fail(SVNRepository::SVN_ERROR_CHECKSUM);
423 } else if (!strcmp(name, SVN_OPEN_DIRECTORY_TAG)) {
424 currentDir->updateReportComplete();
425 if (currentDir->parent()) {
426 // pop the collection stack
427 currentDir = currentDir->parent();
430 currentPath = currentDir->fsPath();
432 // std::cout << "element:" << name;
436 void finishFile(DAVResource* res)
438 res->setVersionName(currentVersionName);
440 currentPath = currentPath.dir();
444 void data (const char * s, int length)
446 if (status != SVNRepository::SVN_NO_ERROR) {
450 if (tagStack.back() == SVN_SET_PROP_TAG) {
451 setPropValue.append(s, length);
452 } else if (tagStack.back() == SVN_TXDELTA_TAG) {
453 txDeltaData.append(s, length);
454 } else if (tagStack.back() == SVN_DAV_MD5_CHECKSUM) {
455 md5Sum.append(s, length);
459 void pi (const char * target, const char * data) {}
461 string tagN(const unsigned int n) const
463 size_t sz = tagStack.size();
468 return tagStack[sz - (1 + n)];
471 void fail(SVNRepository::ResultCode err)
477 DAVCollection* rootCollection;
478 SVNDirectory* currentDir;
479 SVNRepository::ResultCode status;
482 XML_Parser xmlParser;
485 string_list tagStack;
486 string currentVersionName;
491 unsigned int revision;
492 SG_MD5_CTX md5Context;
493 string md5Sum, decodedFileMd5;
494 std::string setPropName, setPropValue;
498 ////////////////////////////////////////////////////////////////////////
499 // Static callback functions for Expat.
500 ////////////////////////////////////////////////////////////////////////
502 #define VISITOR static_cast<SVNReportParser::SVNReportParserPrivate *>(userData)
505 start_element (void * userData, const char * name, const char ** atts)
507 VISITOR->startElement(name, atts);
511 end_element (void * userData, const char * name)
513 VISITOR->endElement(name);
517 character_data (void * userData, const char * s, int len)
519 VISITOR->data(s, len);
523 processing_instruction (void * userData,
527 VISITOR->pi(target, data);
532 ///////////////////////////////////////////////////////////////////////////////
534 SVNReportParser::SVNReportParser(SVNRepository* repo) :
535 _d(new SVNReportParserPrivate(repo))
540 SVNReportParser::~SVNReportParser()
544 SVNRepository::ResultCode
545 SVNReportParser::innerParseXML(const char* data, int size)
547 if (_d->status != SVNRepository::SVN_NO_ERROR) {
551 bool isEnd = (data == NULL);
552 if (!XML_Parse(_d->xmlParser, data, size, isEnd)) {
553 SG_LOG(SG_IO, SG_INFO, "SVN parse error:" << XML_ErrorString(XML_GetErrorCode(_d->xmlParser))
554 << " at line:" << XML_GetCurrentLineNumber(_d->xmlParser)
555 << " column " << XML_GetCurrentColumnNumber(_d->xmlParser));
557 XML_ParserFree(_d->xmlParser);
558 _d->parserInited = false;
559 return SVNRepository::SVN_ERROR_XML;
561 XML_ParserFree(_d->xmlParser);
562 _d->parserInited = false;
568 SVNRepository::ResultCode
569 SVNReportParser::parseXML(const char* data, int size)
571 if (_d->status != SVNRepository::SVN_NO_ERROR) {
575 if (!_d->parserInited) {
576 _d->xmlParser = XML_ParserCreateNS(0, ':');
577 XML_SetUserData(_d->xmlParser, _d.get());
578 XML_SetElementHandler(_d->xmlParser, start_element, end_element);
579 XML_SetCharacterDataHandler(_d->xmlParser, character_data);
580 XML_SetProcessingInstructionHandler(_d->xmlParser, processing_instruction);
581 _d->parserInited = true;
584 return innerParseXML(data, size);
587 SVNRepository::ResultCode SVNReportParser::finishParse()
589 if (_d->status != SVNRepository::SVN_NO_ERROR) {
593 return innerParseXML(NULL, 0);
596 std::string SVNReportParser::etagFromRevision(unsigned int revision)
598 // etags look like W/"7//", hopefully this is stable
599 // across different servers and similar
600 std::ostringstream os;
601 os << "W/\"" << revision << "//";