]> git.mxchange.org Git - simgear.git/blob - simgear/io/SVNReportParser.cxx
Move bundled Expat to new home.
[simgear.git] / simgear / io / SVNReportParser.cxx
1 // SVNReportParser -- parser for SVN report XML data
2 //
3 // Copyright (C) 2012  James Turner <zakalawe@mac.com>
4 //
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.
9 //
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.
14 //
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.
18
19 #ifdef HAVE_CONFIG_H
20 #  include <simgear_config.h>
21 #endif
22      
23 #include "SVNReportParser.hxx"
24
25 #include <iostream>
26 #include <cstring>
27 #include <cassert>
28 #include <algorithm>
29 #include <sstream>
30 #include <fstream>
31
32 #include <boost/foreach.hpp>
33
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"
40
41 #ifdef SYSTEM_EXPAT
42 #  include <expat.h>
43 #else
44 #  include "sg_expat.h"     
45 #endif
46
47 #include "SVNDirectory.hxx"
48 #include "SVNRepository.hxx"
49 #include "DAVMultiStatus.hxx"
50
51 using std::cout;
52 using std::cerr;
53 using std::endl;
54 using std::string;
55
56 using namespace simgear;
57
58 #define DAV_NS "DAV::"
59 #define SVN_NS "svn::"
60 #define SUBVERSION_DAV_NS "http://subversion.tigris.org/xmlns/dav/"
61
62 namespace {
63     
64     #define MAX_ENCODED_INT_LEN 10
65     
66     static size_t
67     decode_size(unsigned char* &p,
68                 const unsigned char *end)
69     {
70       if (p + MAX_ENCODED_INT_LEN < end)
71         end = p + MAX_ENCODED_INT_LEN;
72       /* Decode bytes until we're done.  */
73       size_t result = 0;
74       
75       while (p < end) {
76           result = (result << 7) | (*p & 0x7f);
77           if (((*p++ >> 7) & 0x1) == 0) {
78               break;
79           }
80       }
81       
82       return result;
83     }
84     
85     static bool
86     try_decode_size(unsigned char* &p,
87                 const unsigned char *end)
88     {
89       if (p + MAX_ENCODED_INT_LEN < end)
90         end = p + MAX_ENCODED_INT_LEN;
91
92       while (p < end) {
93           if (((*p++ >> 7) & 0x1) == 0) {
94               return true;
95           }
96       }
97       
98       return false;
99     }
100     
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";
110   
111   const char* SVN_DAV_MD5_CHECKSUM = SUBVERSION_DAV_NS ":md5-checksum";
112   
113   const char* DAV_HREF_TAG = DAV_NS "href";
114   const char* DAV_CHECKED_IN_TAG = SVN_NS "checked-in";
115   
116
117   const int svn_txdelta_source = 0;
118   const int svn_txdelta_target = 1;
119   const int svn_txdelta_new = 2;
120
121   const size_t DELTA_HEADER_SIZE = 4;
122     
123   /**
124    * helper struct to decode and store the SVN delta header
125    * values
126    */
127   struct SVNDeltaWindow
128   {
129   public:
130
131       static bool isWindowComplete(unsigned char* buffer, size_t bytes)
132       {
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)) {
138                   return false;
139               }
140           }
141           
142           p = buffer;
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;
150           
151           return (bytes >= (instructionLen + newLength + headerLength));
152       }
153       
154      SVNDeltaWindow(unsigned char* p) :
155          headerLength(0),
156          _ptr(p)
157      {
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);
163          
164          headerLength = p - _ptr;
165          _ptr = p;
166      }
167   
168     bool apply(std::vector<char>& output, std::istream& source)
169     {
170         unsigned char* pEnd = _ptr + instructionLength;
171         unsigned char* newData = pEnd;
172         
173         while (_ptr < pEnd) {
174           int op = ((*_ptr >> 6) & 0x3);  
175           if (op >= 3) {
176             SG_LOG(SG_IO, SG_INFO, "SVNDeltaWindow: bad opcode:" << op);
177               return false;
178           }
179       
180           int length = *_ptr++ & 0x3f;
181           int offset = 0;
182         
183           if (length == 0) {
184             length = decode_size(_ptr, pEnd);
185           }
186         
187           if (length == 0) {
188                         SG_LOG(SG_IO, SG_INFO, "SVNDeltaWindow: malformed stream, 0 length" << op);
189             return false;
190           }
191       
192           // if op != new, decode another size value
193           if (op != svn_txdelta_new) {
194             offset = decode_size(_ptr, pEnd);
195           }
196
197           if (op == svn_txdelta_target) {
198             while (length > 0) {
199               output.push_back(output[offset++]);
200               --length;
201             }
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);
207             assert(sourceBuf);
208             source.read(sourceBuf, length);
209             output.insert(output.end(), sourceBuf, sourceBuf + length);
210             free(sourceBuf);
211           }
212         } // of instruction loop
213         
214         return true;
215     }  
216   
217     size_t size() const
218     {
219         return headerLength + instructionLength + newLength;
220     }
221   
222     unsigned int sourceViewOffset;
223     size_t sourceViewLength,
224       targetViewLength;
225     size_t headerLength,
226         instructionLength,
227         newLength;
228     
229 private:  
230     unsigned char* _ptr;
231   };
232   
233   
234 } // of anonymous namespace
235
236 class SVNReportParser::SVNReportParserPrivate
237 {
238 public:
239   SVNReportParserPrivate(SVNRepository* repo) :
240     tree(repo),
241     status(SVNRepository::SVN_NO_ERROR),
242     parserInited(false),
243     currentPath(repo->fsBase())
244   {
245     inFile = false;
246     currentDir = repo->rootDir();
247   }
248
249   ~SVNReportParserPrivate()
250   {
251   }
252   
253   void startElement (const char * name, const char** attributes)
254   {    
255       if (status != SVNRepository::SVN_NO_ERROR) {
256           return;
257       }
258       
259     ExpatAtts attrs(attributes);
260     tagStack.push_back(name);
261     if (!strcmp(name, SVN_TXDELTA_TAG)) {
262         txDeltaData.clear();
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;
267       inFile = true;
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;
272       
273       DAVResource* res = currentDir->collection()->childWithName(fileName);   
274       if (!res || !filePath.exists()) {
275         // set error condition
276       }
277       
278       inFile = true;
279     } else if (!strcmp(name, SVN_ADD_DIRECTORY_TAG)) {
280       string dirName(attrs.getValue("name"));
281       Dir d(currentDir->fsDir().file(dirName));
282       if (d.exists()) {
283           // policy decision : if we're doing an add, wipe the existing
284           d.remove(true);
285       }
286     
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)) {
295       md5Sum.clear();
296     } else if (!strcmp(name, SVN_OPEN_DIRECTORY_TAG)) {
297         string dirName;
298         if (attrs.getValue("name")) {
299             dirName = string(attrs.getValue("name"));
300         }
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
307     } else {
308         //std::cerr << "unhandled element:" << name << std::endl;
309     }
310   } // of startElement
311   
312   void openDirectory(const std::string& dirName)
313   {
314       if (dirName.empty()) {
315           // root directory, we shall assume
316           currentDir = tree->rootDir();
317       } else {
318           assert(currentDir);
319           currentDir = currentDir->child(dirName);
320       }
321       
322       assert(currentDir);
323       currentPath = currentDir->fsPath();
324       currentDir->beginUpdateReport();
325   }
326   
327   void deleteEntry(const std::string& entryName)
328   {
329       currentDir->deleteChildByName(entryName);
330   }
331   
332   bool decodeTextDelta(const SGPath& outputPath)
333   {    
334     string decoded = strutils::decodeBase64(txDeltaData);
335     size_t bytesToDecode = decoded.size();
336     std::vector<char> output;      
337     unsigned char* p = (unsigned char*) decoded.data();
338     if (memcmp(p, "SVN\0", DELTA_HEADER_SIZE) != 0) {
339         return false; // bad header
340     }
341     
342     bytesToDecode -= DELTA_HEADER_SIZE;
343     p += DELTA_HEADER_SIZE;
344     std::ifstream source;
345     source.open(outputPath.c_str(), std::ios::in | std::ios::binary);
346     
347     while (bytesToDecode > 0) {  
348         if (!SVNDeltaWindow::isWindowComplete(p, bytesToDecode)) {
349           SG_LOG(SG_IO, SG_WARN, "SVN txdelta broken window");
350           return false;
351         }
352         
353         SVNDeltaWindow window(p);      
354         assert(bytesToDecode >= window.size());
355         window.apply(output, source);
356         bytesToDecode -= window.size();
357         p += window.size();
358     }
359
360     source.close();
361
362     std::ofstream f;
363     f.open(outputPath.c_str(), 
364       std::ios::out | std::ios::trunc | std::ios::binary);
365     f.write(output.data(), output.size());
366
367     // compute MD5 while we have the file in memory
368     memset(&md5Context, 0, sizeof(SG_MD5_CTX));
369     SG_MD5Init(&md5Context);
370     SG_MD5Update(&md5Context, (unsigned char*) output.data(), output.size());
371     SG_MD5Final(&md5Context);
372     decodedFileMd5 = strutils::encodeHex(md5Context.digest, 16);
373
374     return true;
375   }
376   
377   void endElement (const char * name)
378   {
379       if (status != SVNRepository::SVN_NO_ERROR) {
380           return;
381       }
382       
383     assert(tagStack.back() == name);
384     tagStack.pop_back();
385         
386     if (!strcmp(name, SVN_TXDELTA_TAG)) {
387       if (!decodeTextDelta(currentPath)) {
388         fail(SVNRepository::SVN_ERROR_TXDELTA);
389       }
390     } else if (!strcmp(name, SVN_ADD_FILE_TAG)) {
391       finishFile(currentDir->addChildFile(currentPath.file()));
392     } else if (!strcmp(name, SVN_OPEN_FILE_TAG)) {
393       DAVResource* res = currentDir->collection()->childWithName(currentPath.file());   
394       assert(res);
395       finishFile(res);
396     } else if (!strcmp(name, SVN_ADD_DIRECTORY_TAG)) {
397       // pop directory
398       currentPath = currentPath.dir();
399       currentDir->updateReportComplete();
400       currentDir = currentDir->parent();
401     } else if (!strcmp(name, SVN_SET_PROP_TAG)) {
402       if (setPropName == "svn:entry:committed-rev") {
403         revision = strutils::to_int(setPropValue);
404         currentVersionName = setPropValue;
405         if (!inFile) {
406           // for directories we have the resource already
407           // for adding files, we might not; we set the version name
408           // above when ending the add/open-file element
409           currentDir->collection()->setVersionName(currentVersionName);
410         } 
411       }
412     } else if (!strcmp(name, SVN_DAV_MD5_CHECKSUM)) {
413       // validate against (presumably) just written file
414       if (decodedFileMd5 != md5Sum) {
415         fail(SVNRepository::SVN_ERROR_CHECKSUM);
416       }
417     } else if (!strcmp(name, SVN_OPEN_DIRECTORY_TAG)) {
418         if (currentDir->parent()) {   
419           // pop the collection stack
420           currentDir = currentDir->parent();
421         }
422         
423         currentDir->updateReportComplete();
424         currentPath = currentDir->fsPath();
425     } else {
426     //  std::cout << "element:" << name;
427     }
428   }
429   
430   void finishFile(DAVResource* res)
431   {
432       res->setVersionName(currentVersionName);
433       res->setMD5(md5Sum);
434       currentPath = currentPath.dir();
435       inFile = false;
436   }
437   
438   void data (const char * s, int length)
439   {
440       if (status != SVNRepository::SVN_NO_ERROR) {
441           return;
442       }
443       
444     if (tagStack.back() == SVN_SET_PROP_TAG) {
445       setPropValue += string(s, length);
446     } else if (tagStack.back() == SVN_TXDELTA_TAG) {
447       txDeltaData += string(s, length);
448     } else if (tagStack.back() == SVN_DAV_MD5_CHECKSUM) {
449       md5Sum += string(s, length);
450     }
451   }
452   
453   void pi (const char * target, const char * data) {}
454   
455   string tagN(const unsigned int n) const
456   {
457     size_t sz = tagStack.size();
458     if (n >= sz) {
459       return string();
460     }
461     
462     return tagStack[sz - (1 + n)];
463   }
464   
465   void fail(SVNRepository::ResultCode err)
466   {
467       status = err;
468   }
469   
470   SVNRepository* tree;
471   DAVCollection* rootCollection;
472   SVNDirectory* currentDir;
473   SVNRepository::ResultCode status;
474   
475   bool parserInited;
476   XML_Parser xmlParser;
477   
478 // in-flight data
479   string_list tagStack;
480   string currentVersionName;
481   string txDeltaData;
482   SGPath currentPath;
483   bool inFile;
484     
485   unsigned int revision;
486   SG_MD5_CTX md5Context;
487   string md5Sum, decodedFileMd5;
488   std::string setPropName, setPropValue;
489 };
490
491
492 ////////////////////////////////////////////////////////////////////////
493 // Static callback functions for Expat.
494 ////////////////////////////////////////////////////////////////////////
495
496 #define VISITOR static_cast<SVNReportParser::SVNReportParserPrivate *>(userData)
497
498 static void
499 start_element (void * userData, const char * name, const char ** atts)
500 {
501   VISITOR->startElement(name, atts);
502 }
503
504 static void
505 end_element (void * userData, const char * name)
506 {
507   VISITOR->endElement(name);
508 }
509
510 static void
511 character_data (void * userData, const char * s, int len)
512 {
513   VISITOR->data(s, len);
514 }
515
516 static void
517 processing_instruction (void * userData,
518                         const char * target,
519                         const char * data)
520 {
521   VISITOR->pi(target, data);
522 }
523
524 #undef VISITOR
525
526 ///////////////////////////////////////////////////////////////////////////////
527
528 SVNReportParser::SVNReportParser(SVNRepository* repo) :
529   _d(new SVNReportParserPrivate(repo))
530 {
531   
532 }
533
534 SVNReportParser::~SVNReportParser()
535 {
536 }
537
538 SVNRepository::ResultCode
539 SVNReportParser::innerParseXML(const char* data, int size)
540 {
541     if (_d->status != SVNRepository::SVN_NO_ERROR) {
542         return _d->status;
543     }
544     
545     bool isEnd = (data == NULL);
546     if (!XML_Parse(_d->xmlParser, data, size, isEnd)) {
547       SG_LOG(SG_IO, SG_INFO, "SVN parse error:" << XML_ErrorString(XML_GetErrorCode(_d->xmlParser))
548              << " at line:" << XML_GetCurrentLineNumber(_d->xmlParser)
549              << " column " << XML_GetCurrentColumnNumber(_d->xmlParser));
550     
551       XML_ParserFree(_d->xmlParser);
552       _d->parserInited = false;
553       return SVNRepository::SVN_ERROR_XML;
554     } else if (isEnd) {
555         XML_ParserFree(_d->xmlParser);
556         _d->parserInited = false;
557     }
558
559     return _d->status;
560 }
561
562 SVNRepository::ResultCode
563 SVNReportParser::parseXML(const char* data, int size)
564 {
565     if (_d->status != SVNRepository::SVN_NO_ERROR) {
566         return _d->status;
567     }
568     
569   if (!_d->parserInited) {
570     _d->xmlParser = XML_ParserCreateNS(0, ':');
571     XML_SetUserData(_d->xmlParser, _d.get());
572     XML_SetElementHandler(_d->xmlParser, start_element, end_element);
573     XML_SetCharacterDataHandler(_d->xmlParser, character_data);
574     XML_SetProcessingInstructionHandler(_d->xmlParser, processing_instruction);
575     _d->parserInited = true;
576   }
577   
578   return innerParseXML(data, size);
579 }
580
581 SVNRepository::ResultCode SVNReportParser::finishParse()
582 {
583     if (_d->status != SVNRepository::SVN_NO_ERROR) {
584         return _d->status;
585     }
586     
587     return innerParseXML(NULL, 0);
588 }
589
590 std::string SVNReportParser::etagFromRevision(unsigned int revision)
591 {
592   // etags look like W/"7//", hopefully this is stable
593   // across different servers and similar
594   std::ostringstream os;
595   os << "W/\"" << revision << "//";
596   return os.str();
597 }
598
599