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.
19 #include "SVNReportParser.hxx"
28 #include <boost/foreach.hpp>
30 #include "simgear/misc/sg_path.hxx"
31 #include "simgear/misc/sg_dir.hxx"
32 #include "simgear/debug/logstream.hxx"
33 #include "simgear/xml/xmlparse.h"
34 #include "simgear/xml/easyxml.hxx"
35 #include "simgear/misc/strutils.hxx"
36 #include "simgear/package/md5.h"
38 #include "SVNDirectory.hxx"
39 #include "SVNRepository.hxx"
40 #include "DAVMultiStatus.hxx"
47 using namespace simgear;
49 #define DAV_NS "DAV::"
50 #define SVN_NS "svn::"
51 #define SUBVERSION_DAV_NS "http://subversion.tigris.org/xmlns/dav/"
55 #define MAX_ENCODED_INT_LEN 10
58 decode_size(unsigned char* &p,
59 const unsigned char *end)
61 if (p + MAX_ENCODED_INT_LEN < end)
62 end = p + MAX_ENCODED_INT_LEN;
63 /* Decode bytes until we're done. */
67 result = (result << 7) | (*p & 0x7f);
68 if (((*p++ >> 7) & 0x1) == 0) {
77 try_decode_size(unsigned char* &p,
78 const unsigned char *end)
80 if (p + MAX_ENCODED_INT_LEN < end)
81 end = p + MAX_ENCODED_INT_LEN;
84 if (((*p++ >> 7) & 0x1) == 0) {
92 // const char* SVN_UPDATE_REPORT_TAG = SVN_NS "update-report";
93 // const char* SVN_TARGET_REVISION_TAG = SVN_NS "target-revision";
94 const char* SVN_OPEN_DIRECTORY_TAG = SVN_NS "open-directory";
95 const char* SVN_OPEN_FILE_TAG = SVN_NS "open-file";
96 const char* SVN_ADD_DIRECTORY_TAG = SVN_NS "add-directory";
97 const char* SVN_ADD_FILE_TAG = SVN_NS "add-file";
98 const char* SVN_TXDELTA_TAG = SVN_NS "txdelta";
99 const char* SVN_SET_PROP_TAG = SVN_NS "set-prop";
100 const char* SVN_DELETE_ENTRY_TAG = SVN_NS "delete-entry";
102 const char* SVN_DAV_MD5_CHECKSUM = SUBVERSION_DAV_NS ":md5-checksum";
104 const char* DAV_HREF_TAG = DAV_NS "href";
105 const char* DAV_CHECKED_IN_TAG = SVN_NS "checked-in";
108 const int svn_txdelta_source = 0;
109 const int svn_txdelta_target = 1;
110 const int svn_txdelta_new = 2;
112 const size_t DELTA_HEADER_SIZE = 4;
115 * helper struct to decode and store the SVN delta header
118 struct SVNDeltaWindow
122 static bool isWindowComplete(unsigned char* buffer, size_t bytes)
124 unsigned char* p = buffer;
125 unsigned char* pEnd = p + bytes;
126 // if we can't decode five sizes, certainly incomplete
127 for (int i=0; i<5; i++) {
128 if (!try_decode_size(p, pEnd)) {
134 // ignore these three
135 decode_size(p, pEnd);
136 decode_size(p, pEnd);
137 decode_size(p, pEnd);
138 size_t instructionLen = decode_size(p, pEnd);
139 size_t newLength = decode_size(p, pEnd);
140 size_t headerLength = p - buffer;
142 return (bytes >= (instructionLen + newLength + headerLength));
145 SVNDeltaWindow(unsigned char* p) :
149 sourceViewOffset = decode_size(p, p+20);
150 sourceViewLength = decode_size(p, p+20);
151 targetViewLength = decode_size(p, p+20);
152 instructionLength = decode_size(p, p+20);
153 newLength = decode_size(p, p+20);
155 headerLength = p - _ptr;
158 if (sourceViewOffset != 0) {
159 cout << "sourceViewOffset:" << sourceViewOffset << endl;
163 bool apply(std::vector<char>& output, std::istream& source)
165 unsigned char* pEnd = _ptr + instructionLength;
166 unsigned char* newData = pEnd;
168 while (_ptr < pEnd) {
169 int op = ((*_ptr >> 6) & 0x3);
171 std::cerr << "weird opcode" << endl;
175 int length = *_ptr++ & 0x3f;
179 length = decode_size(_ptr, pEnd);
183 std::cerr << "malformed stream, 0 length" << std::endl;
187 // if op != new, decode another size value
188 if (op != svn_txdelta_new) {
189 offset = decode_size(_ptr, pEnd);
192 if (op == svn_txdelta_target) {
194 output.push_back(output[offset++]);
197 } else if (op == svn_txdelta_new) {
198 output.insert(output.end(), newData, newData + length);
199 } else if (op == svn_txdelta_source) {
200 source.seekg(offset);
201 char* sourceBuf = (char*) malloc(length);
203 source.read(sourceBuf, length);
204 output.insert(output.end(), sourceBuf, sourceBuf + length);
207 } // of instruction loop
214 return headerLength + instructionLength + newLength;
217 unsigned int sourceViewOffset;
218 size_t sourceViewLength,
229 } // of anonymous namespace
231 class SVNReportParser::SVNReportParserPrivate
234 SVNReportParserPrivate(SVNRepository* repo) :
236 status(SVNRepository::NO_ERROR),
238 currentPath(repo->fsBase())
241 currentDir = repo->rootDir();
244 ~SVNReportParserPrivate()
248 void startElement (const char * name, const char** attributes)
250 if (status != SVNRepository::NO_ERROR) {
254 ExpatAtts attrs(attributes);
255 tagStack.push_back(name);
256 if (!strcmp(name, SVN_TXDELTA_TAG)) {
258 } else if (!strcmp(name, SVN_ADD_FILE_TAG)) {
259 string fileName(attrs.getValue("name"));
260 SGPath filePath(currentDir->fsDir().file(fileName));
261 currentPath = filePath;
263 } else if (!strcmp(name, SVN_OPEN_FILE_TAG)) {
264 string fileName(attrs.getValue("name"));
265 SGPath filePath(Dir(currentPath).file(fileName));
266 currentPath = filePath;
268 DAVResource* res = currentDir->collection()->childWithName(fileName);
269 if (!res || !filePath.exists()) {
270 // set error condition
274 } else if (!strcmp(name, SVN_ADD_DIRECTORY_TAG)) {
275 string dirName(attrs.getValue("name"));
276 Dir d(currentDir->fsDir().file(dirName));
278 // policy decision : if we're doing an add, wipe the existing
282 currentDir = currentDir->addChildDirectory(dirName);
283 currentPath = currentDir->fsPath();
284 currentDir->beginUpdateReport();
285 //cout << "addDir:" << currentPath << endl;
286 } else if (!strcmp(name, SVN_SET_PROP_TAG)) {
287 setPropName = attrs.getValue("name");
288 setPropValue.clear();
289 } else if (!strcmp(name, SVN_DAV_MD5_CHECKSUM)) {
291 } else if (!strcmp(name, SVN_OPEN_DIRECTORY_TAG)) {
293 if (attrs.getValue("name")) {
294 dirName = string(attrs.getValue("name"));
296 openDirectory(dirName);
297 } else if (!strcmp(name, SVN_DELETE_ENTRY_TAG)) {
298 string entryName(attrs.getValue("name"));
299 deleteEntry(entryName);
300 } else if (!strcmp(name, DAV_CHECKED_IN_TAG) || !strcmp(name, DAV_HREF_TAG)) {
301 // don't warn on these ones
303 //std::cerr << "unhandled element:" << name << std::endl;
307 void openDirectory(const std::string& dirName)
309 if (dirName.empty()) {
310 // root directory, we shall assume
311 currentDir = tree->rootDir();
314 currentDir = currentDir->child(dirName);
318 currentPath = currentDir->fsPath();
319 currentDir->beginUpdateReport();
322 void deleteEntry(const std::string& entryName)
324 currentDir->deleteChildByName(entryName);
327 bool decodeTextDelta(const SGPath& outputPath)
329 string decoded = strutils::decodeBase64(txDeltaData);
330 size_t bytesToDecode = decoded.size();
331 std::vector<char> output;
332 unsigned char* p = (unsigned char*) decoded.data();
333 if (memcmp(decoded.data(), "SVN\0", DELTA_HEADER_SIZE) != 0) {
334 return false; // bad header
337 bytesToDecode -= DELTA_HEADER_SIZE;
338 p += DELTA_HEADER_SIZE;
339 std::ifstream source;
340 source.open(outputPath.c_str(), std::ios::in | std::ios::binary);
342 while (bytesToDecode > 0) {
343 SVNDeltaWindow window(p);
344 assert(bytesToDecode >= window.size());
345 window.apply(output, source);
346 bytesToDecode -= window.size();
352 f.open(outputPath.c_str(),
353 std::ios::out | std::ios::trunc | std::ios::binary);
354 f.write(output.data(), output.size());
356 // compute MD5 while we have the file in memory
358 memset(&md5, 0, sizeof(MD5_CTX));
360 MD5Update(&md5, (unsigned char*) output.data(), output.size());
362 decodedFileMd5 = strutils::encodeHex(md5.digest, 16);
366 void endElement (const char * name)
368 if (status != SVNRepository::NO_ERROR) {
372 assert(tagStack.back() == name);
374 if (!strcmp(name, SVN_TXDELTA_TAG)) {
375 if (!decodeTextDelta(currentPath)) {
376 fail(SVNRepository::ERROR_TXDELTA);
378 } else if (!strcmp(name, SVN_ADD_FILE_TAG)) {
379 finishFile(currentDir->addChildFile(currentPath.file()));
380 } else if (!strcmp(name, SVN_OPEN_FILE_TAG)) {
381 DAVResource* res = currentDir->collection()->childWithName(currentPath.file());
384 } else if (!strcmp(name, SVN_ADD_DIRECTORY_TAG)) {
386 currentPath = currentPath.dir();
387 currentDir->updateReportComplete();
388 currentDir = currentDir->parent();
389 } else if (!strcmp(name, SVN_SET_PROP_TAG)) {
390 if (setPropName == "svn:entry:committed-rev") {
391 revision = strutils::to_int(setPropValue);
392 currentVersionName = setPropValue;
394 // for directories we have the resource already
395 // for adding files, we might not; we set the version name
396 // above when ending the add/open-file element
397 currentDir->collection()->setVersionName(currentVersionName);
400 } else if (!strcmp(name, SVN_DAV_MD5_CHECKSUM)) {
401 // validate against (presumably) just written file
402 if (decodedFileMd5 != md5Sum) {
403 fail(SVNRepository::ERROR_CHECKSUM);
405 } else if (!strcmp(name, SVN_OPEN_DIRECTORY_TAG)) {
406 if (currentDir->parent()) {
407 // pop the collection stack
408 currentDir = currentDir->parent();
411 currentDir->updateReportComplete();
412 currentPath = currentDir->fsPath();
414 // std::cout << "element:" << name;
418 void finishFile(DAVResource* res)
420 res->setVersionName(currentVersionName);
422 currentPath = currentPath.dir();
426 void data (const char * s, int length)
428 if (status != SVNRepository::NO_ERROR) {
432 if (tagStack.back() == SVN_SET_PROP_TAG) {
433 setPropValue += string(s, length);
434 } else if (tagStack.back() == SVN_TXDELTA_TAG) {
435 txDeltaData += string(s, length);
436 } else if (tagStack.back() == SVN_DAV_MD5_CHECKSUM) {
437 md5Sum += string(s, length);
441 void pi (const char * target, const char * data) {}
443 string tagN(const unsigned int n) const
445 size_t sz = tagStack.size();
450 return tagStack[sz - (1 + n)];
453 void fail(SVNRepository::ResultCode err)
459 DAVCollection* rootCollection;
460 SVNDirectory* currentDir;
461 SVNRepository::ResultCode status;
464 XML_Parser xmlParser;
467 string_list tagStack;
468 string currentVersionName;
473 unsigned int revision;
474 string md5Sum, decodedFileMd5;
475 std::string setPropName, setPropValue;
479 ////////////////////////////////////////////////////////////////////////
480 // Static callback functions for Expat.
481 ////////////////////////////////////////////////////////////////////////
483 #define VISITOR static_cast<SVNReportParser::SVNReportParserPrivate *>(userData)
486 start_element (void * userData, const char * name, const char ** atts)
488 VISITOR->startElement(name, atts);
492 end_element (void * userData, const char * name)
494 VISITOR->endElement(name);
498 character_data (void * userData, const char * s, int len)
500 VISITOR->data(s, len);
504 processing_instruction (void * userData,
508 VISITOR->pi(target, data);
513 ///////////////////////////////////////////////////////////////////////////////
515 SVNReportParser::SVNReportParser(SVNRepository* repo) :
516 _d(new SVNReportParserPrivate(repo))
521 SVNReportParser::~SVNReportParser()
525 SVNRepository::ResultCode
526 SVNReportParser::innerParseXML(const char* data, int size)
528 if (_d->status != SVNRepository::NO_ERROR) {
532 bool isEnd = (data == NULL);
533 if (!XML_Parse(_d->xmlParser, data, size, isEnd)) {
534 SG_LOG(SG_IO, SG_INFO, "SVN parse error:" << XML_ErrorString(XML_GetErrorCode(_d->xmlParser))
535 << " at line:" << XML_GetCurrentLineNumber(_d->xmlParser)
536 << " column " << XML_GetCurrentColumnNumber(_d->xmlParser));
538 XML_ParserFree(_d->xmlParser);
539 _d->parserInited = false;
540 return SVNRepository::ERROR_XML;
542 XML_ParserFree(_d->xmlParser);
543 _d->parserInited = false;
549 SVNRepository::ResultCode
550 SVNReportParser::parseXML(const char* data, int size)
552 if (_d->status != SVNRepository::NO_ERROR) {
556 if (!_d->parserInited) {
557 _d->xmlParser = XML_ParserCreateNS(0, ':');
558 XML_SetUserData(_d->xmlParser, _d.get());
559 XML_SetElementHandler(_d->xmlParser, start_element, end_element);
560 XML_SetCharacterDataHandler(_d->xmlParser, character_data);
561 XML_SetProcessingInstructionHandler(_d->xmlParser, processing_instruction);
562 _d->parserInited = true;
565 return innerParseXML(data, size);
568 SVNRepository::ResultCode SVNReportParser::finishParse()
570 if (_d->status != SVNRepository::NO_ERROR) {
574 return innerParseXML(NULL, 0);
577 std::string SVNReportParser::etagFromRevision(unsigned int revision)
579 // etags look like W/"7//", hopefully this is stable
580 // across different servers and similar
581 std::ostringstream os;
582 os << "W/\"" << revision << "//";