Repository: scons/scons
Issue: Empty file and missing file have same csig.

This issue was originally created at: 2015-11-19 12:21:07.
This issue was reported by: `wblevins001`.
wblevins001 said at 2015-11-19 12:21:07
>While working on other patches I discovered that the csig for a FS Node is the same for empty files and missing files.
> 
>From a correctness standpoint, the hash for these files should not be the same.
> 
>Take for example a lock file (just an empty file used as a lock for other processes like side-effects). If an empty file and a missing file have the same csig, then there are possible issues.
> 
>This shouldn't be hard to fix, but it might cause rebuilds.

wblevins001 said at 2016-01-14 17:13:03
>I looked into this; the md5 function doesn't handle passing in None as a parameter; thus, this probably isn't worth pursuing.

wblevins001 said at 2016-01-14 17:13:54
>Unless we want to write None to the cache? I'm not sure if that's a good idea, but it would be accurate...


Votes for this issue: 1.


Changed files:
SCons/Node/FSTests.py, SCons/Util.py, SCons/Node/FS.py

diff --git a/SCons/Node/FS.py b/SCons/Node/FS.py
index 5228dc4fd..290fcb3cc 100644
--- a/SCons/Node/FS.py
+++ b/SCons/Node/FS.py
@@ -2693,11 +2693,13 @@ class File(Base):
     def scanner_key(self):
         return self.get_suffix()
 
-    def get_contents(self):
+    def get_contents(self) -> bytes:
+        """Return the contents of the file as bytes."""
         return SCons.Node._get_contents_map[self._func_get_contents](self)
 
-    def get_text_contents(self):
-        """
+    def get_text_contents(self) -> str:
+        """Return the contents of the file in text form.
+
         This attempts to figure out what the encoding of the text is
         based upon the BOM bytes, and then decodes the contents so that
         it's a valid python string.
@@ -2724,12 +2726,10 @@ class File(Base):
                 return contents.decode('utf-8', errors='backslashreplace')
 
 
-    def get_content_hash(self):
-        """
-        Compute and return the MD5 hash for this file.
-        """
+    def get_content_hash(self) -> str:
+        """Compute and return the hash of the file contents."""
         if not self.rexists():
-            return MD5signature('')
+            return MD5signature(SCons.Util.NOFILE)
         fname = self.rfile().get_abspath()
         try:
             cs = MD5filesignature(fname, chunksize=File.md5_chunksize)
@@ -2740,7 +2740,7 @@ class File(Base):
         return cs
 
     @SCons.Memoize.CountMethodCall
-    def get_size(self):
+    def get_size(self) -> int:
         try:
             return self._memo['get_size']
         except KeyError:
@@ -2749,14 +2749,14 @@ class File(Base):
         if self.rexists():
             size = self.rfile().getsize()
         else:
-            size = 0
+            # sentinel value for doesn't exist, even in repository
+            size = -1
 
         self._memo['get_size'] = size
-
         return size
 
     @SCons.Memoize.CountMethodCall
-    def get_timestamp(self):
+    def get_timestamp(self) -> int:
         try:
             return self._memo['get_timestamp']
         except KeyError:
@@ -2768,7 +2768,6 @@ class File(Base):
             timestamp = 0
 
         self._memo['get_timestamp'] = timestamp
-
         return timestamp
 
     convert_copy_attrs = [
@@ -2780,7 +2779,6 @@ class File(Base):
         'ninfo',
     ]
 
-
     convert_sig_attrs = [
         'bsourcesigs',
         'bimplicitsigs',
@@ -3173,7 +3171,7 @@ class File(Base):
     # SIGNATURE SUBSYSTEM
     #
 
-    def get_max_drift_csig(self):
+    def get_max_drift_csig(self) -> str:
         """
         Returns the content signature currently stored for this node
         if it's been unmodified longer than the max_drift value, or the
@@ -3199,15 +3197,8 @@ class File(Base):
 
         return None
 
-    def get_csig(self):
-        """
-        Generate a node's content signature, the digested signature
-        of its content.
-
-        node - the node
-        cache - alternate node to use for the signature cache
-        returns - the content signature
-        """
+    def get_csig(self) -> str:
+        """Generate a node's content signature."""
         ninfo = self.get_ninfo()
         try:
             return ninfo.csig
@@ -3216,9 +3207,11 @@ class File(Base):
 
         csig = self.get_max_drift_csig()
         if csig is None:
-
             try:
-                if self.get_size() < File.md5_chunksize:
+                size = self.get_size()
+                if size == -1:
+                    contents = SCons.Util.NOFILE
+                elif size < File.md5_chunksize:
                     contents = self.get_contents()
                 else:
                     csig = self.get_content_hash()
diff --git a/SCons/Node/FSTests.py b/SCons/Node/FSTests.py
index 65cc363df..17914c1a5 100644
--- a/SCons/Node/FSTests.py
+++ b/SCons/Node/FSTests.py
@@ -2686,8 +2686,11 @@ class FileTestCase(_tempdirTestCase):
             print("%15s -> csig:%s" % (i3.name, i3.ninfo.csig))
             print("%15s -> csig:%s" % (i4.name, i4.ninfo.csig))
 
-        self.assertEqual(i2.name, i2.ninfo.csig,
-                         "gamma.h's fake csig should equal gamma.h but equals:%s" % i2.ninfo.csig)
+        self.assertEqual(
+            i2.name,
+            i2.ninfo.csig,
+            "gamma.h's fake csig should equal gamma.h but equals:%s" % i2.ninfo.csig,
+        )
 
 
 class GlobTestCase(_tempdirTestCase):
@@ -3673,7 +3676,8 @@ class CacheDirTestCase(unittest.TestCase):
 
         f9 = fs.File('f9')
         r = f9.get_cachedir_csig()
-        assert r == 'd41d8cd98f00b204e9800998ecf8427e', r
+        exsig = SCons.Util.MD5signature(SCons.Util.NOFILE)
+        assert r == exsig, r
 
 
 class clearTestCase(unittest.TestCase):
diff --git a/SCons/Util.py b/SCons/Util.py
index 88aeaae73..fae2d64fe 100644
--- a/SCons/Util.py
+++ b/SCons/Util.py
@@ -35,6 +35,10 @@ from types import MethodType, FunctionType
 
 PYPY = hasattr(sys, 'pypy_translation_info')
 
+# this string will be hashed if a Node refers to a file that doesn't exist
+# in order to distinguish from a file that exists but is empty.
+NOFILE = "SCONS_MAGIC_MISSING_FILE_STRING"
+
 # unused?
 def dictify(keys, values, result=None):
     if result is None: