]> git.ipfire.org Git - thirdparty/git.git/blobdiff - path.c
Sync with 2.16.6
[thirdparty/git.git] / path.c
diff --git a/path.c b/path.c
index da8b655730d363dda5010bdf2d53bd76abb82931..84e88243a9a2f7de14e02217df247a78fe68fc91 100644 (file)
--- a/path.c
+++ b/path.c
@@ -1289,37 +1289,161 @@ int daemon_avoid_alias(const char *p)
        }
 }
 
-static int only_spaces_and_periods(const char *path, size_t len, size_t skip)
+/*
+ * On NTFS, we need to be careful to disallow certain synonyms of the `.git/`
+ * directory:
+ *
+ * - For historical reasons, file names that end in spaces or periods are
+ *   automatically trimmed. Therefore, `.git . . ./` is a valid way to refer
+ *   to `.git/`.
+ *
+ * - For other historical reasons, file names that do not conform to the 8.3
+ *   format (up to eight characters for the basename, three for the file
+ *   extension, certain characters not allowed such as `+`, etc) are associated
+ *   with a so-called "short name", at least on the `C:` drive by default.
+ *   Which means that `git~1/` is a valid way to refer to `.git/`.
+ *
+ *   Note: Technically, `.git/` could receive the short name `git~2` if the
+ *   short name `git~1` were already used. In Git, however, we guarantee that
+ *   `.git` is the first item in a directory, therefore it will be associated
+ *   with the short name `git~1` (unless short names are disabled).
+ *
+ * - For yet other historical reasons, NTFS supports so-called "Alternate Data
+ *   Streams", i.e. metadata associated with a given file, referred to via
+ *   `<filename>:<stream-name>:<stream-type>`. There exists a default stream
+ *   type for directories, allowing `.git/` to be accessed via
+ *   `.git::$INDEX_ALLOCATION/`.
+ *
+ * When this function returns 1, it indicates that the specified file/directory
+ * name refers to a `.git` file or directory, or to any of these synonyms, and
+ * Git should therefore not track it.
+ *
+ * For performance reasons, _all_ Alternate Data Streams of `.git/` are
+ * forbidden, not just `::$INDEX_ALLOCATION`.
+ *
+ * This function is intended to be used by `git fsck` even on platforms where
+ * the backslash is a regular filename character, therefore it needs to handle
+ * backlash characters in the provided `name` specially: they are interpreted
+ * as directory separators.
+ */
+int is_ntfs_dotgit(const char *name)
 {
-       if (len < skip)
+       char c;
+
+       /*
+        * Note that when we don't find `.git` or `git~1` we end up with `name`
+        * advanced partway through the string. That's okay, though, as we
+        * return immediately in those cases, without looking at `name` any
+        * further.
+        */
+       c = *(name++);
+       if (c == '.') {
+               /* .git */
+               if (((c = *(name++)) != 'g' && c != 'G') ||
+                   ((c = *(name++)) != 'i' && c != 'I') ||
+                   ((c = *(name++)) != 't' && c != 'T'))
+                       return 0;
+       } else if (c == 'g' || c == 'G') {
+               /* git ~1 */
+               if (((c = *(name++)) != 'i' && c != 'I') ||
+                   ((c = *(name++)) != 't' && c != 'T') ||
+                   *(name++) != '~' ||
+                   *(name++) != '1')
+                       return 0;
+       } else
                return 0;
-       len -= skip;
-       path += skip;
-       while (len-- > 0) {
-               char c = *(path++);
-               if (c != ' ' && c != '.')
+
+       for (;;) {
+               c = *(name++);
+               if (!c || c == '\\' || c == '/' || c == ':')
+                       return 1;
+               if (c != '.' && c != ' ')
                        return 0;
        }
-       return 1;
 }
 
-int is_ntfs_dotgit(const char *name)
+static int is_ntfs_dot_generic(const char *name,
+                              const char *dotgit_name,
+                              size_t len,
+                              const char *dotgit_ntfs_shortname_prefix)
 {
-       int len;
-
-       for (len = 0; ; len++)
-               if (!name[len] || name[len] == '\\' || is_dir_sep(name[len])) {
-                       if (only_spaces_and_periods(name, len, 4) &&
-                                       !strncasecmp(name, ".git", 4))
-                               return 1;
-                       if (only_spaces_and_periods(name, len, 5) &&
-                                       !strncasecmp(name, "git~1", 5))
+       int saw_tilde;
+       size_t i;
+
+       if ((name[0] == '.' && !strncasecmp(name + 1, dotgit_name, len))) {
+               i = len + 1;
+only_spaces_and_periods:
+               for (;;) {
+                       char c = name[i++];
+                       if (!c || c == ':')
                                return 1;
-                       if (name[len] != '\\')
+                       if (c != ' ' && c != '.')
                                return 0;
-                       name += len + 1;
-                       len = -1;
                }
+       }
+
+       /*
+        * Is it a regular NTFS short name, i.e. shortened to 6 characters,
+        * followed by ~1, ... ~4?
+        */
+       if (!strncasecmp(name, dotgit_name, 6) && name[6] == '~' &&
+           name[7] >= '1' && name[7] <= '4') {
+               i = 8;
+               goto only_spaces_and_periods;
+       }
+
+       /*
+        * Is it a fall-back NTFS short name (for details, see
+        * https://en.wikipedia.org/wiki/8.3_filename?
+        */
+       for (i = 0, saw_tilde = 0; i < 8; i++)
+               if (name[i] == '\0')
+                       return 0;
+               else if (saw_tilde) {
+                       if (name[i] < '0' || name[i] > '9')
+                               return 0;
+               } else if (name[i] == '~') {
+                       if (name[++i] < '1' || name[i] > '9')
+                               return 0;
+                       saw_tilde = 1;
+               } else if (i >= 6)
+                       return 0;
+               else if (name[i] < 0) {
+                       /*
+                        * We know our needles contain only ASCII, so we clamp
+                        * here to make the results of tolower() sane.
+                        */
+                       return 0;
+               } else if (tolower(name[i]) != dotgit_ntfs_shortname_prefix[i])
+                       return 0;
+
+       goto only_spaces_and_periods;
+}
+
+/*
+ * Inline helper to make sure compiler resolves strlen() on literals at
+ * compile time.
+ */
+static inline int is_ntfs_dot_str(const char *name, const char *dotgit_name,
+                                 const char *dotgit_ntfs_shortname_prefix)
+{
+       return is_ntfs_dot_generic(name, dotgit_name, strlen(dotgit_name),
+                                  dotgit_ntfs_shortname_prefix);
+}
+
+int is_ntfs_dotgitmodules(const char *name)
+{
+       return is_ntfs_dot_str(name, "gitmodules", "gi7eba");
+}
+
+int is_ntfs_dotgitignore(const char *name)
+{
+       return is_ntfs_dot_str(name, "gitignore", "gi250a");
+}
+
+int is_ntfs_dotgitattributes(const char *name)
+{
+       return is_ntfs_dot_str(name, "gitattributes", "gi7d29");
 }
 
 int looks_like_command_line_option(const char *str)