2
0
Эх сурвалжийг харах

protect against circular patch chains

David Rose 16 жил өмнө
parent
commit
6b0f19f13e

+ 62 - 37
direct/src/p3d/PatchMaker.py

@@ -23,9 +23,10 @@ class PatchMaker:
             self.printName = None
             self.printName = None
 
 
             # The Package object that produces this version, if this
             # The Package object that produces this version, if this
-            # is the current form or the base form, respectively.
+            # is the current, base, or top form, respectively.
             self.packageCurrent = None
             self.packageCurrent = None
             self.packageBase = None
             self.packageBase = None
+            self.packageTop = None
 
 
             # A list of patchfiles that can produce this version.
             # A list of patchfiles that can produce this version.
             self.fromPatches = []
             self.fromPatches = []
@@ -41,7 +42,7 @@ class PatchMaker:
             if self.tempFile:
             if self.tempFile:
                 self.tempFile.unlink()
                 self.tempFile.unlink()
 
 
-        def getPatchChain(self, startPv):
+        def getPatchChain(self, startPv, alreadyVisited = []):
             """ Returns a list of patches that, when applied in
             """ Returns a list of patches that, when applied in
             sequence to the indicated PackageVersion object, will
             sequence to the indicated PackageVersion object, will
             produce this PackageVersion object.  Returns None if no
             produce this PackageVersion object.  Returns None if no
@@ -51,11 +52,18 @@ class PatchMaker:
                 # We're already here.  A zero-length patch chain is
                 # We're already here.  A zero-length patch chain is
                 # therefore the answer.
                 # therefore the answer.
                 return []
                 return []
+            if self in alreadyVisited:
+                # We've already been here; this is a loop.  Avoid
+                # infinite recursion.
+                return None
+
+            alreadyVisited = alreadyVisited[:]
+            alreadyVisited.append(self)
 
 
             bestPatchChain = None
             bestPatchChain = None
             for patchfile in self.fromPatches:
             for patchfile in self.fromPatches:
                 fromPv = patchfile.fromPv
                 fromPv = patchfile.fromPv
-                patchChain = fromPv.getPatchChain(startPv)
+                patchChain = fromPv.getPatchChain(startPv, alreadyVisited)
                 if patchChain is not None:
                 if patchChain is not None:
                     # There's a path through this patchfile.
                     # There's a path through this patchfile.
                     patchChain = patchChain + [patchfile]
                     patchChain = patchChain + [patchfile]
@@ -66,19 +74,27 @@ class PatchMaker:
             # paths found.
             # paths found.
             return bestPatchChain
             return bestPatchChain
 
 
-        def getRecreateFilePlan(self):
+        def getRecreateFilePlan(self, alreadyVisited = []):
             """ Returns the tuple (startFile, startPv, plan),
             """ Returns the tuple (startFile, startPv, plan),
             describing how to recreate the archive file for this
             describing how to recreate the archive file for this
             version.  startFile and startPv is the Filename and
             version.  startFile and startPv is the Filename and
             packageVersion of the file to start with, and plan is a
             packageVersion of the file to start with, and plan is a
             list of tuples (patchfile, pv), listing the patches to
             list of tuples (patchfile, pv), listing the patches to
             apply in sequence, and the packageVersion object
             apply in sequence, and the packageVersion object
-            associated with each patch.  Returns (None, None) if there
-            is no way to recreate this archive file.  """
+            associated with each patch.  Returns (None, None, None) if
+            there is no way to recreate this archive file.  """
             
             
             if self.tempFile:
             if self.tempFile:
                 return (self.tempFile, self, [])
                 return (self.tempFile, self, [])
 
 
+            if self in alreadyVisited:
+                # We've already been here; this is a loop.  Avoid
+                # infinite recursion.
+                return (None, None, None)
+
+            alreadyVisited = alreadyVisited[:]
+            alreadyVisited.append(self)
+
             if self.packageCurrent:
             if self.packageCurrent:
                 # This PackageVersion instance represents the current
                 # This PackageVersion instance represents the current
                 # version of some package.
                 # version of some package.
@@ -97,7 +113,7 @@ class PatchMaker:
             bestStartPv = None
             bestStartPv = None
             for patchfile in self.fromPatches:
             for patchfile in self.fromPatches:
                 fromPv = patchfile.fromPv
                 fromPv = patchfile.fromPv
-                startFile, startPv, plan = fromPv.getRecreateFilePlan()
+                startFile, startPv, plan = fromPv.getRecreateFilePlan(alreadyVisited)
                 if plan is not None:
                 if plan is not None:
                     # There's a path through this patchfile.
                     # There's a path through this patchfile.
                     plan = plan + [(patchfile, self)]
                     plan = plan + [(patchfile, self)]
@@ -279,6 +295,7 @@ class PatchMaker:
             self.patchVersion = 1
             self.patchVersion = 1
             self.currentPv = None
             self.currentPv = None
             self.basePv = None
             self.basePv = None
+            self.topPv = None
 
 
             self.packageName = None
             self.packageName = None
             self.platform = None
             self.platform = None
@@ -303,14 +320,22 @@ class PatchMaker:
             
             
             return (self.packageName, self.platform, self.version, self.hostUrl, self.baseFile)
             return (self.packageName, self.platform, self.version, self.hostUrl, self.baseFile)
 
 
+        def getTopKey(self):
+            """ Returns the key to locate the "top" or newest version
+            of this package. """
+            
+            return (self.packageName, self.platform, self.version, self.hostUrl, self.topFile)
+
         def getGenericKey(self, fileSpec):
         def getGenericKey(self, fileSpec):
             """ Returns the key that has the indicated hash. """
             """ Returns the key that has the indicated hash. """
             return (self.packageName, self.platform, self.version, self.hostUrl, fileSpec)
             return (self.packageName, self.platform, self.version, self.hostUrl, fileSpec)
 
 
-        def readDescFile(self):
+        def readDescFile(self, doProcessing = False):
             """ Reads the existing package.xml file and stores it in
             """ Reads the existing package.xml file and stores it in
-            this class for later rewriting.  Returns true on success,
-            false on failure. """
+            this class for later rewriting.  if doProcessing is true,
+            it may massage the file and the directory contents in
+            preparation for building patches.  Returns true on
+            success, false on failure. """
 
 
             self.anyChanges = False
             self.anyChanges = False
 
 
@@ -361,7 +386,6 @@ class PatchMaker:
                     isNewVersion = False
                     isNewVersion = False
                 else:
                 else:
                     # There's a new version this pass.  Update it.
                     # There's a new version this pass.  Update it.
-                    self.topFile = copy.copy(self.currentFile)
                     self.anyChanges = True
                     self.anyChanges = True
                 
                 
             else:
             else:
@@ -399,15 +423,18 @@ class PatchMaker:
                 compressedFile.loadXml(xcompressed)
                 compressedFile.loadXml(xcompressed)
 
 
                 oldCompressedFilename = compressedFile.filename
                 oldCompressedFilename = compressedFile.filename
-                newCompressedFilename = '%s.%s.pz' % (self.currentFile.filename, self.patchVersion)
-                oldCompressedPathname = Filename(self.packageDir, oldCompressedFilename)
-                newCompressedPathname = Filename(self.packageDir, newCompressedFilename)
-                if oldCompressedPathname.renameTo(newCompressedPathname):
-                    compressedFile.fromFile(self.packageDir, newCompressedFilename)
-                    compressedFile.storeXml(xcompressed)
-
-                self.compressedFilename = newCompressedFilename
-                self.anyChanges = True
+                self.compressedFilename = oldCompressedFilename
+
+                if doProcessing:
+                    newCompressedFilename = '%s.%s.pz' % (self.currentFile.filename, self.patchVersion)
+                    oldCompressedPathname = Filename(self.packageDir, oldCompressedFilename)
+                    newCompressedPathname = Filename(self.packageDir, newCompressedFilename)
+                    if oldCompressedPathname.renameTo(newCompressedPathname):
+                        compressedFile.fromFile(self.packageDir, newCompressedFilename)
+                        compressedFile.storeXml(xcompressed)
+
+                    self.compressedFilename = newCompressedFilename
+                    self.anyChanges = True
 
 
             # Get the base_version--the bottom (oldest) of the patch
             # Get the base_version--the bottom (oldest) of the patch
             # chain.
             # chain.
@@ -429,7 +456,7 @@ class PatchMaker:
                 self.baseFile.filename += '.base'
                 self.baseFile.filename += '.base'
 
 
                 # Also duplicate the (compressed) file itself.
                 # Also duplicate the (compressed) file itself.
-                if self.compressedFilename:
+                if doProcessing and self.compressedFilename:
                     fromPathname = Filename(self.packageDir, self.compressedFilename)
                     fromPathname = Filename(self.packageDir, self.compressedFilename)
                     toPathname = Filename(self.packageDir, self.baseFile.filename + '.pz')
                     toPathname = Filename(self.packageDir, self.baseFile.filename + '.pz')
                     fromPathname.copyTo(toPathname)
                     fromPathname.copyTo(toPathname)
@@ -460,7 +487,7 @@ class PatchMaker:
             # Remove all of the old patch entries from the desc file
             # Remove all of the old patch entries from the desc file
             # we read earlier.
             # we read earlier.
             xremove = []
             xremove = []
-            for value in ['base_version', 'patch']:
+            for value in ['base_version', 'top_version', 'patch']:
                 xpatch = xpackage.FirstChildElement(value)
                 xpatch = xpackage.FirstChildElement(value)
                 while xpatch:
                 while xpatch:
                     xremove.append(xpatch)
                     xremove.append(xpatch)
@@ -478,8 +505,9 @@ class PatchMaker:
             self.baseFile.storeXml(xarchive)
             self.baseFile.storeXml(xarchive)
             xpackage.InsertEndChild(xarchive)
             xpackage.InsertEndChild(xarchive)
 
 
+            # The current version is now the top version.
             xarchive = TiXmlElement('top_version')
             xarchive = TiXmlElement('top_version')
-            self.topFile.storeXml(xarchive)
+            self.currentFile.storeXml(xarchive)
             xpackage.InsertEndChild(xarchive)
             xpackage.InsertEndChild(xarchive)
             
             
             for patchfile in self.patches:
             for patchfile in self.patches:
@@ -554,7 +582,7 @@ class PatchMaker:
         object, or None on failure. """
         object, or None on failure. """
 
 
         package = self.Package(Filename(descFilename), self)
         package = self.Package(Filename(descFilename), self)
-        if not package.readDescFile():
+        if not package.readDescFile(doProcessing = False):
             return None
             return None
         
         
         self.packages.append(package)
         self.packages.append(package)
@@ -581,7 +609,7 @@ class PatchMaker:
                 if filename and not solo:
                 if filename and not solo:
                     filename = Filename(filename)
                     filename = Filename(filename)
                     package = self.Package(filename, self, xpackage)
                     package = self.Package(filename, self, xpackage)
-                    package.readDescFile()
+                    package.readDescFile(doProcessing = True)
                     self.packages.append(package)
                     self.packages.append(package)
                     
                     
                 xpackage = xpackage.NextSiblingElement('package')
                 xpackage = xpackage.NextSiblingElement('package')
@@ -638,6 +666,10 @@ class PatchMaker:
             package.basePv = basePv
             package.basePv = basePv
             basePv.packageBase = package
             basePv.packageBase = package
             basePv.printName = package.baseFile.filename
             basePv.printName = package.baseFile.filename
+
+            topPv = self.getPackageVersion(package.getTopKey())
+            package.topPv = topPv
+            topPv.packageTop = package
             
             
             for patchfile in package.patches:
             for patchfile in package.patches:
                 self.recordPatchfile(patchfile)
                 self.recordPatchfile(patchfile)
@@ -682,22 +714,15 @@ class PatchMaker:
             # No versions.
             # No versions.
             return
             return
 
 
-        # Starting with the package base, how far forward can we go?
+        # What's the current version on the top of the tree?
+        topPv = package.topPv
         currentPv = package.currentPv
         currentPv = package.currentPv
-        basePv = package.basePv
-
-        pv = basePv
-        nextPv = pv.getNext(package)
-        while nextPv:
-            pv = nextPv
-            nextPv = pv.getNext(package)
         
         
-        if pv.packageCurrent != package:
-            # It doesn't reach all the way to the latest version, so
-            # build a new patch.
+        if topPv != currentPv:
+            # They're different, so build a new patch.
             filename = Filename(package.currentFile.filename + '.%s.patch' % (package.patchVersion))
             filename = Filename(package.currentFile.filename + '.%s.patch' % (package.patchVersion))
             assert filename not in self.patchFilenames
             assert filename not in self.patchFilenames
-            if not self.buildPatch(pv, currentPv, package, filename):
+            if not self.buildPatch(topPv, currentPv, package, filename):
                 raise StandardError, "Couldn't build patch."
                 raise StandardError, "Couldn't build patch."
 
 
     def buildPatch(self, v1, v2, package, patchFilename):
     def buildPatch(self, v1, v2, package, patchFilename):

+ 17 - 3
direct/src/plugin/p3dPatchFinder.cxx

@@ -40,13 +40,27 @@ PackageVersion(const PackageVersionKey &key) :
 //               false if no chain can be found.
 //               false if no chain can be found.
 ////////////////////////////////////////////////////////////////////
 ////////////////////////////////////////////////////////////////////
 bool P3DPatchFinder::PackageVersion::
 bool P3DPatchFinder::PackageVersion::
-get_patch_chain(Patchfiles &chain, PackageVersion *start_pv) {
+get_patch_chain(Patchfiles &chain, PackageVersion *start_pv,
+                const PackageVersionsList &already_visited_in) {
   chain.clear();
   chain.clear();
   if (this == start_pv) {
   if (this == start_pv) {
     // We're already here.  A zero-length patch chain is therefore the
     // We're already here.  A zero-length patch chain is therefore the
     // answer.
     // answer.
     return true;
     return true;
   }
   }
+  if (::find(already_visited_in.begin(), already_visited_in.end(), this) != already_visited_in.end()) {
+    // We've already been here; this is a loop.  Avoid infinite
+    // recursion.
+    return false;
+  }
+
+  // Yeah, we make a new copy of this vector at each stage of the
+  // recursion.  This could be made much faster with a linked list
+  // instead, but I'm working on the assumption that there will be no
+  // more than a few dozen patchfiles, in which case this naive
+  // approach should be fast enough.
+  PackageVersionsList already_visited = already_visited_in;
+  already_visited.push_back(this);
 
 
   bool found_any = false;
   bool found_any = false;
   Patchfiles::iterator pi;
   Patchfiles::iterator pi;
@@ -55,7 +69,7 @@ get_patch_chain(Patchfiles &chain, PackageVersion *start_pv) {
     PackageVersion *from_pv = patchfile->_from_pv;
     PackageVersion *from_pv = patchfile->_from_pv;
     assert(from_pv != NULL);
     assert(from_pv != NULL);
     Patchfiles this_chain;
     Patchfiles this_chain;
-    if (from_pv->get_patch_chain(this_chain, start_pv)) {
+    if (from_pv->get_patch_chain(this_chain, start_pv, already_visited)) {
       // There's a path through this patchfile.
       // There's a path through this patchfile.
       this_chain.push_back(patchfile);
       this_chain.push_back(patchfile);
       if (!found_any || this_chain.size() < chain.size()) {
       if (!found_any || this_chain.size() < chain.size()) {
@@ -341,7 +355,7 @@ get_patch_chain_to_current(Patchfiles &chain, TiXmlDocument *doc,
   PackageVersion *to_pv = package->_current_pv;
   PackageVersion *to_pv = package->_current_pv;
 
 
   if (to_pv != NULL && from_pv != NULL) {
   if (to_pv != NULL && from_pv != NULL) {
-    return to_pv->get_patch_chain(chain, from_pv);
+    return to_pv->get_patch_chain(chain, from_pv, PackageVersionsList());
   }
   }
 
 
   return false;
   return false;

+ 4 - 1
direct/src/plugin/p3dPatchFinder.h

@@ -35,8 +35,10 @@ class P3DPatchFinder {
 public:
 public:
   class Package;
   class Package;
   class Patchfile;
   class Patchfile;
+  class PackageVersion;
 
 
   typedef vector<Patchfile *> Patchfiles;
   typedef vector<Patchfile *> Patchfiles;
+  typedef vector<PackageVersion *> PackageVersionsList;
 
 
   // This class is used to index into a map to locate PackageVersion
   // This class is used to index into a map to locate PackageVersion
   // objects, below.
   // objects, below.
@@ -65,7 +67,8 @@ public:
   public:
   public:
     PackageVersion(const PackageVersionKey &key);
     PackageVersion(const PackageVersionKey &key);
 
 
-    bool get_patch_chain(Patchfiles &chain, PackageVersion *start_pv);
+    bool get_patch_chain(Patchfiles &chain, PackageVersion *start_pv,
+                         const PackageVersionsList &already_visited_in);
 
 
   public:
   public:
     string _package_name;
     string _package_name;