]> git.mxchange.org Git - simgear.git/blob - simgear/io/SVNReportParser.cxx
Don't use object returned from vector::end()
[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_PROP_TAG = SVN_NS "prop";
110   const char* SVN_DELETE_ENTRY_TAG = SVN_NS "delete-entry";
111   
112   const char* SVN_DAV_MD5_CHECKSUM = SUBVERSION_DAV_NS ":md5-checksum";
113   
114   const char* DAV_HREF_TAG = DAV_NS "href";
115   const char* DAV_CHECKED_IN_TAG = DAV_NS "checked-in";
116   
117
118   const int svn_txdelta_source = 0;
119   const int svn_txdelta_target = 1;
120   const int svn_txdelta_new = 2;
121
122   const size_t DELTA_HEADER_SIZE = 4;
123     
124   /**
125    * helper struct to decode and store the SVN delta header
126    * values
127    */
128   struct SVNDeltaWindow
129   {
130   public:
131
132       static bool isWindowComplete(unsigned char* buffer, size_t bytes)
133       {
134           unsigned char* p = buffer;
135           unsigned char* pEnd = p + bytes;
136           // if we can't decode five sizes, certainly incomplete
137           for (int i=0; i<5; i++) {
138               if (!try_decode_size(p, pEnd)) {
139                   return false;
140               }
141           }
142           
143           p = buffer;
144         // ignore these three
145           decode_size(p, pEnd);
146           decode_size(p, pEnd);
147           decode_size(p, pEnd);
148           size_t instructionLen = decode_size(p, pEnd);
149           size_t newLength = decode_size(p, pEnd);
150           size_t headerLength = p - buffer;
151           
152           return (bytes >= (instructionLen + newLength + headerLength));
153       }
154       
155      SVNDeltaWindow(unsigned char* p) :
156          headerLength(0),
157          _ptr(p)
158      {
159          sourceViewOffset = decode_size(p, p+20);
160          sourceViewLength = decode_size(p, p+20);
161          targetViewLength = decode_size(p, p+20);
162          instructionLength = decode_size(p, p+20);
163          newLength = decode_size(p, p+20);
164          
165          headerLength = p - _ptr;
166          _ptr = p;
167      }
168   
169     bool apply(std::vector<unsigned char>& output, std::istream& source)
170     {
171         unsigned char* pEnd = _ptr + instructionLength;
172         unsigned char* newData = pEnd;
173         
174         while (_ptr < pEnd) {
175           int op = ((*_ptr >> 6) & 0x3);  
176           if (op >= 3) {
177               SG_LOG(SG_IO, SG_INFO, "SVNDeltaWindow: bad opcode:" << op);
178               return false;
179           }
180       
181           int length = *_ptr++ & 0x3f;
182           int offset = 0;
183         
184           if (length == 0) {
185             length = decode_size(_ptr, pEnd);
186           }
187         
188           if (length == 0) {
189                         SG_LOG(SG_IO, SG_INFO, "SVNDeltaWindow: malformed stream, 0 length" << op);
190             return false;
191           }
192       
193           // if op != new, decode another size value
194           if (op != svn_txdelta_new) {
195             offset = decode_size(_ptr, pEnd);
196           }
197
198           if (op == svn_txdelta_target) {
199               // this is inefficent, but ranges can overlap.
200               while (length > 0) {
201                   output.push_back(output[offset++]);
202                   --length;
203               }
204           } else if (op == svn_txdelta_new) {
205               output.insert(output.end(), newData, newData + length);
206               newData += length;
207           } else if (op == svn_txdelta_source) {
208             source.seekg(offset);
209             char* sourceBuf = (char*) malloc(length);
210             assert(sourceBuf);
211             source.read(sourceBuf, length);
212             output.insert(output.end(), sourceBuf, sourceBuf + length);
213             free(sourceBuf);
214           } else {
215               SG_LOG(SG_IO, SG_WARN, "bad opcode logic");
216               return false;
217           }
218         } // of instruction loop
219         
220         return true;
221     }  
222   
223     size_t size() const
224     {
225         return headerLength + instructionLength + newLength;
226     }
227   
228     unsigned int sourceViewOffset;
229     size_t sourceViewLength,
230       targetViewLength;
231     size_t headerLength,
232         instructionLength,
233         newLength;
234     
235 private:  
236     unsigned char* _ptr;
237   };
238   
239   
240 } // of anonymous namespace
241
242 class SVNReportParser::SVNReportParserPrivate
243 {
244 public:
245   SVNReportParserPrivate(SVNRepository* repo) :
246     tree(repo),
247     status(AbstractRepository::REPO_NO_ERROR),
248     parserInited(false),
249     currentPath(repo->fsBase())
250   {
251     inFile = false;
252     currentDir = repo->rootDir();
253   }
254
255   ~SVNReportParserPrivate()
256   {
257   }
258   
259   void startElement (const char * name, const char** attributes)
260   {    
261       if (status != AbstractRepository::REPO_NO_ERROR) {
262           return;
263       }
264       
265     ExpatAtts attrs(attributes);
266     tagStack.push_back(name);
267     if (!strcmp(name, SVN_TXDELTA_TAG)) {
268         txDeltaData.clear();
269     } else if (!strcmp(name, SVN_ADD_FILE_TAG)) {
270       string fileName(attrs.getValue("name"));
271       SGPath filePath(currentDir->fsDir().file(fileName));
272       currentPath = filePath;
273       inFile = true;
274     } else if (!strcmp(name, SVN_OPEN_FILE_TAG)) {
275       string fileName(attrs.getValue("name"));
276       SGPath filePath(Dir(currentPath).file(fileName));
277       currentPath = filePath;
278        
279       if (!filePath.exists()) {
280           fail(AbstractRepository::REPO_ERROR_FILE_NOT_FOUND);
281           return;
282       }
283
284       inFile = true;
285     } else if (!strcmp(name, SVN_ADD_DIRECTORY_TAG)) {
286       string dirName(attrs.getValue("name"));
287       Dir d(currentDir->fsDir().file(dirName));
288       if (d.exists()) {
289           // policy decision : if we're doing an add, wipe the existing
290           d.remove(true);
291       }
292     
293       currentDir = currentDir->addChildDirectory(dirName);
294       currentPath = currentDir->fsPath();
295       currentDir->beginUpdateReport();
296       //cout << "addDir:" << currentPath << endl;
297     } else if (!strcmp(name, SVN_SET_PROP_TAG)) {
298       setPropName = attrs.getValue("name");
299       setPropValue.clear();
300     } else if (!strcmp(name, SVN_DAV_MD5_CHECKSUM)) {
301       md5Sum.clear();
302     } else if (!strcmp(name, SVN_OPEN_DIRECTORY_TAG)) {
303         string dirName;
304         if (attrs.getValue("name")) {
305             dirName = string(attrs.getValue("name"));
306         }
307         openDirectory(dirName);
308     } else if (!strcmp(name, SVN_DELETE_ENTRY_TAG)) {
309         string entryName(attrs.getValue("name"));
310         deleteEntry(entryName);
311     } else if (!strcmp(name, DAV_CHECKED_IN_TAG) ||
312                !strcmp(name, DAV_HREF_TAG) ||
313                !strcmp(name, SVN_PROP_TAG)) {
314         // don't warn on these ones
315     } else {
316         //SG_LOG(SG_IO, SG_WARN, "SVNReportParser: unhandled tag:" << name);
317     }
318   } // of startElement
319   
320   void openDirectory(const std::string& dirName)
321   {
322       if (dirName.empty()) {
323           // root directory, we shall assume
324           currentDir = tree->rootDir();
325       } else {
326           assert(currentDir);
327           currentDir = currentDir->child(dirName);
328       }
329       
330       assert(currentDir);
331       currentPath = currentDir->fsPath();
332       currentDir->beginUpdateReport();
333   }
334   
335   void deleteEntry(const std::string& entryName)
336   {
337       currentDir->deleteChildByName(entryName);
338   }
339   
340   bool decodeTextDelta(const SGPath& outputPath)
341   {
342       std::vector<unsigned char> output, decoded;
343     strutils::decodeBase64(txDeltaData, decoded);
344     size_t bytesToDecode = decoded.size();
345          
346     unsigned char* p = decoded.data();
347     if (memcmp(p, "SVN\0", DELTA_HEADER_SIZE) != 0) {
348         return false; // bad header
349     }
350     
351     bytesToDecode -= DELTA_HEADER_SIZE;
352     p += DELTA_HEADER_SIZE;
353     std::ifstream source;
354     source.open(outputPath.c_str(), std::ios::in | std::ios::binary);
355     
356     while (bytesToDecode > 0) {  
357         if (!SVNDeltaWindow::isWindowComplete(p, bytesToDecode)) {
358             SG_LOG(SG_IO, SG_WARN, "SVN txdelta broken window");
359             return false;
360         }
361         
362         SVNDeltaWindow window(p);      
363         assert(bytesToDecode >= window.size());
364         window.apply(output, source);
365         bytesToDecode -= window.size();
366         p += window.size();
367     }
368
369     source.close();
370
371     std::ofstream f;
372     f.open(outputPath.c_str(), 
373       std::ios::out | std::ios::trunc | std::ios::binary);
374     f.write((char*) output.data(), output.size());
375
376     // compute MD5 while we have the file in memory
377     memset(&md5Context, 0, sizeof(SG_MD5_CTX));
378     SG_MD5Init(&md5Context);
379     SG_MD5Update(&md5Context, (unsigned char*) output.data(), output.size());
380     unsigned char digest[MD5_DIGEST_LENGTH];
381     SG_MD5Final(digest, &md5Context);
382     decodedFileMd5 = strutils::encodeHex(digest, MD5_DIGEST_LENGTH);
383
384     return true;
385   }
386   
387   void endElement (const char * name)
388   {
389       if (status != SVNRepository::REPO_NO_ERROR) {
390           return;
391       }
392       
393     assert(tagStack.back() == name);
394     tagStack.pop_back();
395         
396     if (!strcmp(name, SVN_TXDELTA_TAG)) {
397       if (!decodeTextDelta(currentPath)) {
398         fail(SVNRepository::SVN_ERROR_TXDELTA);
399       }
400     } else if (!strcmp(name, SVN_ADD_FILE_TAG)) {
401       finishFile(currentPath);
402     } else if (!strcmp(name, SVN_OPEN_FILE_TAG)) {
403       finishFile(currentPath);
404     } else if (!strcmp(name, SVN_ADD_DIRECTORY_TAG)) {
405       // pop directory
406       currentPath = currentPath.dir();
407       currentDir->updateReportComplete();
408       currentDir = currentDir->parent();
409     } else if (!strcmp(name, SVN_SET_PROP_TAG)) {
410       if (setPropName == "svn:entry:committed-rev") {
411         revision = strutils::to_int(setPropValue);
412         currentVersionName = setPropValue;
413         if (!inFile) {
414           // for directories we have the resource already
415           // for adding files, we might not; we set the version name
416           // above when ending the add/open-file element
417           currentDir->collection()->setVersionName(currentVersionName);
418         } 
419       }
420     } else if (!strcmp(name, SVN_DAV_MD5_CHECKSUM)) {
421       // validate against (presumably) just written file
422       if (decodedFileMd5 != md5Sum) {
423         fail(SVNRepository::REPO_ERROR_CHECKSUM);
424       }
425     } else if (!strcmp(name, SVN_OPEN_DIRECTORY_TAG)) {
426         currentDir->updateReportComplete();
427         if (currentDir->parent()) {   
428           // pop the collection stack
429           currentDir = currentDir->parent();
430         }
431         
432         currentPath = currentDir->fsPath();
433     } else {
434     //  std::cout << "element:" << name;
435     }
436   }
437   
438   void finishFile(const SGPath& path)
439   {
440       currentPath = path.dir();
441       inFile = false;
442   }
443   
444   void data (const char * s, int length)
445   {
446       if (status != SVNRepository::REPO_NO_ERROR) {
447           return;
448       }
449       
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);
456     }
457   }
458   
459   void pi (const char * target, const char * data) {}
460   
461   string tagN(const unsigned int n) const
462   {
463     size_t sz = tagStack.size();
464     if (n >= sz) {
465       return string();
466     }
467     
468     return tagStack[sz - (1 + n)];
469   }
470   
471   void fail(SVNRepository::ResultCode err)
472   {
473       status = err;
474   }
475   
476   SVNRepository* tree;
477   DAVCollection* rootCollection;
478   SVNDirectory* currentDir;
479   SVNRepository::ResultCode status;
480   
481   bool parserInited;
482   XML_Parser xmlParser;
483   
484 // in-flight data
485   string_list tagStack;
486   string currentVersionName;
487   string txDeltaData;
488   SGPath currentPath;
489   bool inFile;
490     
491   unsigned int revision;
492   SG_MD5_CTX md5Context;
493   string md5Sum, decodedFileMd5;
494   std::string setPropName, setPropValue;
495 };
496
497
498 ////////////////////////////////////////////////////////////////////////
499 // Static callback functions for Expat.
500 ////////////////////////////////////////////////////////////////////////
501
502 #define VISITOR static_cast<SVNReportParser::SVNReportParserPrivate *>(userData)
503
504 static void
505 start_element (void * userData, const char * name, const char ** atts)
506 {
507   VISITOR->startElement(name, atts);
508 }
509
510 static void
511 end_element (void * userData, const char * name)
512 {
513   VISITOR->endElement(name);
514 }
515
516 static void
517 character_data (void * userData, const char * s, int len)
518 {
519   VISITOR->data(s, len);
520 }
521
522 static void
523 processing_instruction (void * userData,
524                         const char * target,
525                         const char * data)
526 {
527   VISITOR->pi(target, data);
528 }
529
530 #undef VISITOR
531
532 ///////////////////////////////////////////////////////////////////////////////
533
534 SVNReportParser::SVNReportParser(SVNRepository* repo) :
535   _d(new SVNReportParserPrivate(repo))
536 {
537   
538 }
539
540 SVNReportParser::~SVNReportParser()
541 {
542 }
543
544 SVNRepository::ResultCode
545 SVNReportParser::innerParseXML(const char* data, int size)
546 {
547     if (_d->status != SVNRepository::REPO_NO_ERROR) {
548         return _d->status;
549     }
550     
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));
556     
557       XML_ParserFree(_d->xmlParser);
558       _d->parserInited = false;
559       return SVNRepository::SVN_ERROR_XML;
560     } else if (isEnd) {
561         XML_ParserFree(_d->xmlParser);
562         _d->parserInited = false;
563     }
564
565     return _d->status;
566 }
567
568 SVNRepository::ResultCode
569 SVNReportParser::parseXML(const char* data, int size)
570 {
571     if (_d->status != SVNRepository::REPO_NO_ERROR) {
572         return _d->status;
573     }
574     
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;
582   }
583   
584   return innerParseXML(data, size);
585 }
586
587 SVNRepository::ResultCode SVNReportParser::finishParse()
588 {
589     if (_d->status != SVNRepository::REPO_NO_ERROR) {
590         return _d->status;
591     }
592     
593     return innerParseXML(NULL, 0);
594 }
595
596 std::string SVNReportParser::etagFromRevision(unsigned int revision)
597 {
598   // etags look like W/"7//", hopefully this is stable
599   // across different servers and similar
600   std::ostringstream os;
601   os << "W/\"" << revision << "//";
602   return os.str();
603 }
604
605