]> git.mxchange.org Git - simgear.git/blob - simgear/io/SVNReportParser.cxx
Rename MD5 code.
[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 #include "SVNReportParser.hxx"
20
21 #include <iostream>
22 #include <cstring>
23 #include <cassert>
24 #include <algorithm>
25 #include <sstream>
26 #include <fstream>
27
28 #include <boost/foreach.hpp>
29
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"
37
38 #include "SVNDirectory.hxx"
39 #include "SVNRepository.hxx"
40 #include "DAVMultiStatus.hxx"
41
42 using std::cout;
43 using std::cerr;
44 using std::endl;
45 using std::string;
46
47 using namespace simgear;
48
49 #define DAV_NS "DAV::"
50 #define SVN_NS "svn::"
51 #define SUBVERSION_DAV_NS "http://subversion.tigris.org/xmlns/dav/"
52
53 namespace {
54     
55     #define MAX_ENCODED_INT_LEN 10
56     
57     static size_t
58     decode_size(unsigned char* &p,
59                 const unsigned char *end)
60     {
61       if (p + MAX_ENCODED_INT_LEN < end)
62         end = p + MAX_ENCODED_INT_LEN;
63       /* Decode bytes until we're done.  */
64       size_t result = 0;
65       
66       while (p < end) {
67           result = (result << 7) | (*p & 0x7f);
68           if (((*p++ >> 7) & 0x1) == 0) {
69               break;
70           }
71       }
72       
73       return result;
74     }
75     
76     static bool
77     try_decode_size(unsigned char* &p,
78                 const unsigned char *end)
79     {
80       if (p + MAX_ENCODED_INT_LEN < end)
81         end = p + MAX_ENCODED_INT_LEN;
82
83       while (p < end) {
84           if (((*p++ >> 7) & 0x1) == 0) {
85               return true;
86           }
87       }
88       
89       return false;
90     }
91     
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";
101   
102   const char* SVN_DAV_MD5_CHECKSUM = SUBVERSION_DAV_NS ":md5-checksum";
103   
104   const char* DAV_HREF_TAG = DAV_NS "href";
105   const char* DAV_CHECKED_IN_TAG = SVN_NS "checked-in";
106   
107
108   const int svn_txdelta_source = 0;
109   const int svn_txdelta_target = 1;
110   const int svn_txdelta_new = 2;
111
112   const size_t DELTA_HEADER_SIZE = 4;
113     
114   /**
115    * helper struct to decode and store the SVN delta header
116    * values
117    */
118   struct SVNDeltaWindow
119   {
120   public:
121
122       static bool isWindowComplete(unsigned char* buffer, size_t bytes)
123       {
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)) {
129                   return false;
130               }
131           }
132           
133           p = buffer;
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;
141           
142           return (bytes >= (instructionLen + newLength + headerLength));
143       }
144       
145      SVNDeltaWindow(unsigned char* p) :
146          headerLength(0),
147          _ptr(p)
148      {
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);
154          
155          headerLength = p - _ptr;
156          _ptr = p;
157      }
158   
159     bool apply(std::vector<char>& output, std::istream& source)
160     {
161         unsigned char* pEnd = _ptr + instructionLength;
162         unsigned char* newData = pEnd;
163         
164         while (_ptr < pEnd) {
165           int op = ((*_ptr >> 6) & 0x3);  
166           if (op >= 3) {
167             SG_LOG(SG_IO, SG_INFO, "SVNDeltaWindow: bad opcode:" << op);
168               return false;
169           }
170       
171           int length = *_ptr++ & 0x3f;
172           int offset = 0;
173         
174           if (length == 0) {
175             length = decode_size(_ptr, pEnd);
176           }
177         
178           if (length == 0) {
179                         SG_LOG(SG_IO, SG_INFO, "SVNDeltaWindow: malformed stream, 0 length" << op);
180             return false;
181           }
182       
183           // if op != new, decode another size value
184           if (op != svn_txdelta_new) {
185             offset = decode_size(_ptr, pEnd);
186           }
187
188           if (op == svn_txdelta_target) {
189             while (length > 0) {
190               output.push_back(output[offset++]);
191               --length;
192             }
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);
198             assert(sourceBuf);
199             source.read(sourceBuf, length);
200             output.insert(output.end(), sourceBuf, sourceBuf + length);
201             free(sourceBuf);
202           }
203         } // of instruction loop
204         
205         return true;
206     }  
207   
208     size_t size() const
209     {
210         return headerLength + instructionLength + newLength;
211     }
212   
213     unsigned int sourceViewOffset;
214     size_t sourceViewLength,
215       targetViewLength;
216     size_t headerLength,
217         instructionLength,
218         newLength;
219     
220 private:  
221     unsigned char* _ptr;
222   };
223   
224   
225 } // of anonymous namespace
226
227 class SVNReportParser::SVNReportParserPrivate
228 {
229 public:
230   SVNReportParserPrivate(SVNRepository* repo) :
231     tree(repo),
232     status(SVNRepository::SVN_NO_ERROR),
233     parserInited(false),
234     currentPath(repo->fsBase())
235   {
236     inFile = false;
237     currentDir = repo->rootDir();
238   }
239
240   ~SVNReportParserPrivate()
241   {
242   }
243   
244   void startElement (const char * name, const char** attributes)
245   {    
246       if (status != SVNRepository::SVN_NO_ERROR) {
247           return;
248       }
249       
250     ExpatAtts attrs(attributes);
251     tagStack.push_back(name);
252     if (!strcmp(name, SVN_TXDELTA_TAG)) {
253         txDeltaData.clear();
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;
258       inFile = true;
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;
263       
264       DAVResource* res = currentDir->collection()->childWithName(fileName);   
265       if (!res || !filePath.exists()) {
266         // set error condition
267       }
268       
269       inFile = true;
270     } else if (!strcmp(name, SVN_ADD_DIRECTORY_TAG)) {
271       string dirName(attrs.getValue("name"));
272       Dir d(currentDir->fsDir().file(dirName));
273       if (d.exists()) {
274           // policy decision : if we're doing an add, wipe the existing
275           d.remove(true);
276       }
277     
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)) {
286       md5Sum.clear();
287     } else if (!strcmp(name, SVN_OPEN_DIRECTORY_TAG)) {
288         string dirName;
289         if (attrs.getValue("name")) {
290             dirName = string(attrs.getValue("name"));
291         }
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
298     } else {
299         //std::cerr << "unhandled element:" << name << std::endl;
300     }
301   } // of startElement
302   
303   void openDirectory(const std::string& dirName)
304   {
305       if (dirName.empty()) {
306           // root directory, we shall assume
307           currentDir = tree->rootDir();
308       } else {
309           assert(currentDir);
310           currentDir = currentDir->child(dirName);
311       }
312       
313       assert(currentDir);
314       currentPath = currentDir->fsPath();
315       currentDir->beginUpdateReport();
316   }
317   
318   void deleteEntry(const std::string& entryName)
319   {
320       currentDir->deleteChildByName(entryName);
321   }
322   
323   bool decodeTextDelta(const SGPath& outputPath)
324   {    
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
331     }
332     
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);
337     
338     while (bytesToDecode > 0) {  
339         if (!SVNDeltaWindow::isWindowComplete(p, bytesToDecode)) {
340           SG_LOG(SG_IO, SG_WARN, "SVN txdelta broken window");
341           return false;
342         }
343         
344         SVNDeltaWindow window(p);      
345         assert(bytesToDecode >= window.size());
346         window.apply(output, source);
347         bytesToDecode -= window.size();
348         p += window.size();
349     }
350
351     source.close();
352
353     std::ofstream f;
354     f.open(outputPath.c_str(), 
355       std::ios::out | std::ios::trunc | std::ios::binary);
356     f.write(output.data(), output.size());
357
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);
364
365     return true;
366   }
367   
368   void endElement (const char * name)
369   {
370       if (status != SVNRepository::SVN_NO_ERROR) {
371           return;
372       }
373       
374     assert(tagStack.back() == name);
375     tagStack.pop_back();
376         
377     if (!strcmp(name, SVN_TXDELTA_TAG)) {
378       if (!decodeTextDelta(currentPath)) {
379         fail(SVNRepository::SVN_ERROR_TXDELTA);
380       }
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());   
385       assert(res);
386       finishFile(res);
387     } else if (!strcmp(name, SVN_ADD_DIRECTORY_TAG)) {
388       // pop directory
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;
396         if (!inFile) {
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);
401         } 
402       }
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);
407       }
408     } else if (!strcmp(name, SVN_OPEN_DIRECTORY_TAG)) {
409         if (currentDir->parent()) {   
410           // pop the collection stack
411           currentDir = currentDir->parent();
412         }
413         
414         currentDir->updateReportComplete();
415         currentPath = currentDir->fsPath();
416     } else {
417     //  std::cout << "element:" << name;
418     }
419   }
420   
421   void finishFile(DAVResource* res)
422   {
423       res->setVersionName(currentVersionName);
424       res->setMD5(md5Sum);
425       currentPath = currentPath.dir();
426       inFile = false;
427   }
428   
429   void data (const char * s, int length)
430   {
431       if (status != SVNRepository::SVN_NO_ERROR) {
432           return;
433       }
434       
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);
441     }
442   }
443   
444   void pi (const char * target, const char * data) {}
445   
446   string tagN(const unsigned int n) const
447   {
448     size_t sz = tagStack.size();
449     if (n >= sz) {
450       return string();
451     }
452     
453     return tagStack[sz - (1 + n)];
454   }
455   
456   void fail(SVNRepository::ResultCode err)
457   {
458       status = err;
459   }
460   
461   SVNRepository* tree;
462   DAVCollection* rootCollection;
463   SVNDirectory* currentDir;
464   SVNRepository::ResultCode status;
465   
466   bool parserInited;
467   XML_Parser xmlParser;
468   
469 // in-flight data
470   string_list tagStack;
471   string currentVersionName;
472   string txDeltaData;
473   SGPath currentPath;
474   bool inFile;
475     
476   unsigned int revision;
477   SG_MD5_CTX md5Context;
478   string md5Sum, decodedFileMd5;
479   std::string setPropName, setPropValue;
480 };
481
482
483 ////////////////////////////////////////////////////////////////////////
484 // Static callback functions for Expat.
485 ////////////////////////////////////////////////////////////////////////
486
487 #define VISITOR static_cast<SVNReportParser::SVNReportParserPrivate *>(userData)
488
489 static void
490 start_element (void * userData, const char * name, const char ** atts)
491 {
492   VISITOR->startElement(name, atts);
493 }
494
495 static void
496 end_element (void * userData, const char * name)
497 {
498   VISITOR->endElement(name);
499 }
500
501 static void
502 character_data (void * userData, const char * s, int len)
503 {
504   VISITOR->data(s, len);
505 }
506
507 static void
508 processing_instruction (void * userData,
509                         const char * target,
510                         const char * data)
511 {
512   VISITOR->pi(target, data);
513 }
514
515 #undef VISITOR
516
517 ///////////////////////////////////////////////////////////////////////////////
518
519 SVNReportParser::SVNReportParser(SVNRepository* repo) :
520   _d(new SVNReportParserPrivate(repo))
521 {
522   
523 }
524
525 SVNReportParser::~SVNReportParser()
526 {
527 }
528
529 SVNRepository::ResultCode
530 SVNReportParser::innerParseXML(const char* data, int size)
531 {
532     if (_d->status != SVNRepository::SVN_NO_ERROR) {
533         return _d->status;
534     }
535     
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));
541     
542       XML_ParserFree(_d->xmlParser);
543       _d->parserInited = false;
544       return SVNRepository::SVN_ERROR_XML;
545     } else if (isEnd) {
546         XML_ParserFree(_d->xmlParser);
547         _d->parserInited = false;
548     }
549
550     return _d->status;
551 }
552
553 SVNRepository::ResultCode
554 SVNReportParser::parseXML(const char* data, int size)
555 {
556     if (_d->status != SVNRepository::SVN_NO_ERROR) {
557         return _d->status;
558     }
559     
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;
567   }
568   
569   return innerParseXML(data, size);
570 }
571
572 SVNRepository::ResultCode SVNReportParser::finishParse()
573 {
574     if (_d->status != SVNRepository::SVN_NO_ERROR) {
575         return _d->status;
576     }
577     
578     return innerParseXML(NULL, 0);
579 }
580
581 std::string SVNReportParser::etagFromRevision(unsigned int revision)
582 {
583   // etags look like W/"7//", hopefully this is stable
584   // across different servers and similar
585   std::ostringstream os;
586   os << "W/\"" << revision << "//";
587   return os.str();
588 }
589
590