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;
159 bool apply(std::vector<char>& output, std::istream& source)
161 unsigned char* pEnd = _ptr + instructionLength;
162 unsigned char* newData = pEnd;
164 while (_ptr < pEnd) {
165 int op = ((*_ptr >> 6) & 0x3);
167 SG_LOG(SG_IO, SG_INFO, "SVNDeltaWindow: bad opcode:" << op);
171 int length = *_ptr++ & 0x3f;
175 length = decode_size(_ptr, pEnd);
179 SG_LOG(SG_IO, SG_INFO, "SVNDeltaWindow: malformed stream, 0 length" << op);
183 // if op != new, decode another size value
184 if (op != svn_txdelta_new) {
185 offset = decode_size(_ptr, pEnd);
188 if (op == svn_txdelta_target) {
190 output.push_back(output[offset++]);
193 } else if (op == svn_txdelta_new) {
194 output.insert(output.end(), newData, newData + length);
195 } else if (op == svn_txdelta_source) {
196 source.seekg(offset);
197 char* sourceBuf = (char*) malloc(length);
199 source.read(sourceBuf, length);
200 output.insert(output.end(), sourceBuf, sourceBuf + length);
203 } // of instruction loop
210 return headerLength + instructionLength + newLength;
213 unsigned int sourceViewOffset;
214 size_t sourceViewLength,
225 } // of anonymous namespace
227 class SVNReportParser::SVNReportParserPrivate
230 SVNReportParserPrivate(SVNRepository* repo) :
232 status(SVNRepository::SVN_NO_ERROR),
234 currentPath(repo->fsBase())
237 currentDir = repo->rootDir();
240 ~SVNReportParserPrivate()
244 void startElement (const char * name, const char** attributes)
246 if (status != SVNRepository::SVN_NO_ERROR) {
250 ExpatAtts attrs(attributes);
251 tagStack.push_back(name);
252 if (!strcmp(name, SVN_TXDELTA_TAG)) {
254 } else if (!strcmp(name, SVN_ADD_FILE_TAG)) {
255 string fileName(attrs.getValue("name"));
256 SGPath filePath(currentDir->fsDir().file(fileName));
257 currentPath = filePath;
259 } else if (!strcmp(name, SVN_OPEN_FILE_TAG)) {
260 string fileName(attrs.getValue("name"));
261 SGPath filePath(Dir(currentPath).file(fileName));
262 currentPath = filePath;
264 DAVResource* res = currentDir->collection()->childWithName(fileName);
265 if (!res || !filePath.exists()) {
266 // set error condition
270 } else if (!strcmp(name, SVN_ADD_DIRECTORY_TAG)) {
271 string dirName(attrs.getValue("name"));
272 Dir d(currentDir->fsDir().file(dirName));
274 // policy decision : if we're doing an add, wipe the existing
278 currentDir = currentDir->addChildDirectory(dirName);
279 currentPath = currentDir->fsPath();
280 currentDir->beginUpdateReport();
281 //cout << "addDir:" << currentPath << endl;
282 } else if (!strcmp(name, SVN_SET_PROP_TAG)) {
283 setPropName = attrs.getValue("name");
284 setPropValue.clear();
285 } else if (!strcmp(name, SVN_DAV_MD5_CHECKSUM)) {
287 } else if (!strcmp(name, SVN_OPEN_DIRECTORY_TAG)) {
289 if (attrs.getValue("name")) {
290 dirName = string(attrs.getValue("name"));
292 openDirectory(dirName);
293 } else if (!strcmp(name, SVN_DELETE_ENTRY_TAG)) {
294 string entryName(attrs.getValue("name"));
295 deleteEntry(entryName);
296 } else if (!strcmp(name, DAV_CHECKED_IN_TAG) || !strcmp(name, DAV_HREF_TAG)) {
297 // don't warn on these ones
299 //std::cerr << "unhandled element:" << name << std::endl;
303 void openDirectory(const std::string& dirName)
305 if (dirName.empty()) {
306 // root directory, we shall assume
307 currentDir = tree->rootDir();
310 currentDir = currentDir->child(dirName);
314 currentPath = currentDir->fsPath();
315 currentDir->beginUpdateReport();
318 void deleteEntry(const std::string& entryName)
320 currentDir->deleteChildByName(entryName);
323 bool decodeTextDelta(const SGPath& outputPath)
325 string decoded = strutils::decodeBase64(txDeltaData);
326 size_t bytesToDecode = decoded.size();
327 std::vector<char> output;
328 unsigned char* p = (unsigned char*) decoded.data();
329 if (memcmp(p, "SVN\0", DELTA_HEADER_SIZE) != 0) {
330 return false; // bad header
333 bytesToDecode -= DELTA_HEADER_SIZE;
334 p += DELTA_HEADER_SIZE;
335 std::ifstream source;
336 source.open(outputPath.c_str(), std::ios::in | std::ios::binary);
338 while (bytesToDecode > 0) {
339 if (!SVNDeltaWindow::isWindowComplete(p, bytesToDecode)) {
340 SG_LOG(SG_IO, SG_WARN, "SVN txdelta broken window");
344 SVNDeltaWindow window(p);
345 assert(bytesToDecode >= window.size());
346 window.apply(output, source);
347 bytesToDecode -= window.size();
354 f.open(outputPath.c_str(),
355 std::ios::out | std::ios::trunc | std::ios::binary);
356 f.write(output.data(), output.size());
358 // compute MD5 while we have the file in memory
359 memset(&md5Context, 0, sizeof(SG_MD5_CTX));
360 SG_MD5Init(&md5Context);
361 SG_MD5Update(&md5Context, (unsigned char*) output.data(), output.size());
362 SG_MD5Final(&md5Context);
363 decodedFileMd5 = strutils::encodeHex(md5Context.digest, 16);
368 void endElement (const char * name)
370 if (status != SVNRepository::SVN_NO_ERROR) {
374 assert(tagStack.back() == name);
377 if (!strcmp(name, SVN_TXDELTA_TAG)) {
378 if (!decodeTextDelta(currentPath)) {
379 fail(SVNRepository::SVN_ERROR_TXDELTA);
381 } else if (!strcmp(name, SVN_ADD_FILE_TAG)) {
382 finishFile(currentDir->addChildFile(currentPath.file()));
383 } else if (!strcmp(name, SVN_OPEN_FILE_TAG)) {
384 DAVResource* res = currentDir->collection()->childWithName(currentPath.file());
387 } else if (!strcmp(name, SVN_ADD_DIRECTORY_TAG)) {
389 currentPath = currentPath.dir();
390 currentDir->updateReportComplete();
391 currentDir = currentDir->parent();
392 } else if (!strcmp(name, SVN_SET_PROP_TAG)) {
393 if (setPropName == "svn:entry:committed-rev") {
394 revision = strutils::to_int(setPropValue);
395 currentVersionName = setPropValue;
397 // for directories we have the resource already
398 // for adding files, we might not; we set the version name
399 // above when ending the add/open-file element
400 currentDir->collection()->setVersionName(currentVersionName);
403 } else if (!strcmp(name, SVN_DAV_MD5_CHECKSUM)) {
404 // validate against (presumably) just written file
405 if (decodedFileMd5 != md5Sum) {
406 fail(SVNRepository::SVN_ERROR_CHECKSUM);
408 } else if (!strcmp(name, SVN_OPEN_DIRECTORY_TAG)) {
409 if (currentDir->parent()) {
410 // pop the collection stack
411 currentDir = currentDir->parent();
414 currentDir->updateReportComplete();
415 currentPath = currentDir->fsPath();
417 // std::cout << "element:" << name;
421 void finishFile(DAVResource* res)
423 res->setVersionName(currentVersionName);
425 currentPath = currentPath.dir();
429 void data (const char * s, int length)
431 if (status != SVNRepository::SVN_NO_ERROR) {
435 if (tagStack.back() == SVN_SET_PROP_TAG) {
436 setPropValue += string(s, length);
437 } else if (tagStack.back() == SVN_TXDELTA_TAG) {
438 txDeltaData += string(s, length);
439 } else if (tagStack.back() == SVN_DAV_MD5_CHECKSUM) {
440 md5Sum += string(s, length);
444 void pi (const char * target, const char * data) {}
446 string tagN(const unsigned int n) const
448 size_t sz = tagStack.size();
453 return tagStack[sz - (1 + n)];
456 void fail(SVNRepository::ResultCode err)
462 DAVCollection* rootCollection;
463 SVNDirectory* currentDir;
464 SVNRepository::ResultCode status;
467 XML_Parser xmlParser;
470 string_list tagStack;
471 string currentVersionName;
476 unsigned int revision;
477 SG_MD5_CTX md5Context;
478 string md5Sum, decodedFileMd5;
479 std::string setPropName, setPropValue;
483 ////////////////////////////////////////////////////////////////////////
484 // Static callback functions for Expat.
485 ////////////////////////////////////////////////////////////////////////
487 #define VISITOR static_cast<SVNReportParser::SVNReportParserPrivate *>(userData)
490 start_element (void * userData, const char * name, const char ** atts)
492 VISITOR->startElement(name, atts);
496 end_element (void * userData, const char * name)
498 VISITOR->endElement(name);
502 character_data (void * userData, const char * s, int len)
504 VISITOR->data(s, len);
508 processing_instruction (void * userData,
512 VISITOR->pi(target, data);
517 ///////////////////////////////////////////////////////////////////////////////
519 SVNReportParser::SVNReportParser(SVNRepository* repo) :
520 _d(new SVNReportParserPrivate(repo))
525 SVNReportParser::~SVNReportParser()
529 SVNRepository::ResultCode
530 SVNReportParser::innerParseXML(const char* data, int size)
532 if (_d->status != SVNRepository::SVN_NO_ERROR) {
536 bool isEnd = (data == NULL);
537 if (!XML_Parse(_d->xmlParser, data, size, isEnd)) {
538 SG_LOG(SG_IO, SG_INFO, "SVN parse error:" << XML_ErrorString(XML_GetErrorCode(_d->xmlParser))
539 << " at line:" << XML_GetCurrentLineNumber(_d->xmlParser)
540 << " column " << XML_GetCurrentColumnNumber(_d->xmlParser));
542 XML_ParserFree(_d->xmlParser);
543 _d->parserInited = false;
544 return SVNRepository::SVN_ERROR_XML;
546 XML_ParserFree(_d->xmlParser);
547 _d->parserInited = false;
553 SVNRepository::ResultCode
554 SVNReportParser::parseXML(const char* data, int size)
556 if (_d->status != SVNRepository::SVN_NO_ERROR) {
560 if (!_d->parserInited) {
561 _d->xmlParser = XML_ParserCreateNS(0, ':');
562 XML_SetUserData(_d->xmlParser, _d.get());
563 XML_SetElementHandler(_d->xmlParser, start_element, end_element);
564 XML_SetCharacterDataHandler(_d->xmlParser, character_data);
565 XML_SetProcessingInstructionHandler(_d->xmlParser, processing_instruction);
566 _d->parserInited = true;
569 return innerParseXML(data, size);
572 SVNRepository::ResultCode SVNReportParser::finishParse()
574 if (_d->status != SVNRepository::SVN_NO_ERROR) {
578 return innerParseXML(NULL, 0);
581 std::string SVNReportParser::etagFromRevision(unsigned int revision)
583 // etags look like W/"7//", hopefully this is stable
584 // across different servers and similar
585 std::ostringstream os;
586 os << "W/\"" << revision << "//";