Add implementations of various extension related methods (derived from file_util):
Extension, RemoveExtension, InsertBeforeExtension, ReplaceExtension

I didn't reimplement the old file_util ones since they actually modify the FilePath in place, which isn't the style of the rest of the FilePath methods.  I'll file a cleanup bug after this for callers to switch to the new methods.

Review URL: http://codereview.chromium.org/17243

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@7811 0039d316-1c4b-4281-b951-d872f2087c98


CrOS-Libchrome-Original-Commit: 235c17a4631e92a4cbd214863758165a2f573801
diff --git a/base/file_path.cc b/base/file_path.cc
index 9a645b0..8738c37 100644
--- a/base/file_path.cc
+++ b/base/file_path.cc
@@ -21,6 +21,9 @@
 const FilePath::CharType FilePath::kCurrentDirectory[] = FILE_PATH_LITERAL(".");
 const FilePath::CharType FilePath::kParentDirectory[] = FILE_PATH_LITERAL("..");
 
+const FilePath::CharType FilePath::kExtensionSeparator = FILE_PATH_LITERAL('.');
+
+
 namespace {
 
 // If this FilePath contains a drive letter specification, returns the
@@ -38,10 +41,8 @@
        (path[0] >= L'a' && path[0] <= L'z'))) {
     return 1;
   }
-  return FilePath::StringType::npos;
-#else  // FILE_PATH_USES_DRIVE_LETTERS
-  return FilePath::StringType::npos;
 #endif  // FILE_PATH_USES_DRIVE_LETTERS
+  return FilePath::StringType::npos;
 }
 
 bool IsPathAbsolute(const FilePath::StringType& path) {
@@ -136,7 +137,83 @@
   return new_path;
 }
 
-FilePath FilePath::Append(const FilePath::StringType& component) const {
+FilePath::StringType FilePath::Extension() const {
+  // BaseName() calls StripTrailingSeparators, so cases like /foo.baz/// work.
+  StringType base = BaseName().value();
+
+  // Special case "." and ".."
+  if (base == kCurrentDirectory || base == kParentDirectory)
+    return StringType();
+
+  const StringType::size_type last_dot = base.rfind(kExtensionSeparator);
+  if (last_dot == StringType::npos)
+    return StringType();
+  return StringType(base, last_dot);
+}
+
+FilePath FilePath::RemoveExtension() const {
+  StringType ext = Extension();
+  // It's important to check Extension() since that verifies that the
+  // kExtensionSeparator actually appeared in the last path component.
+  if (ext.empty())
+    return FilePath(path_);
+  // Since Extension() verified that the extension is in fact in the last path
+  // component, this substr will effectively strip trailing separators.
+  const StringType::size_type last_dot = path_.rfind(kExtensionSeparator);
+  return FilePath(path_.substr(0, last_dot));
+}
+
+FilePath FilePath::InsertBeforeExtension(const StringType& suffix) const {
+  if (suffix.empty())
+    return FilePath(path_);
+
+  if (path_.empty())
+    return FilePath();
+
+  StringType base = BaseName().value();
+  if (base.empty())
+    return FilePath();
+  if (*(base.end() - 1) == kExtensionSeparator) {
+    // Special case "." and ".."
+    if (base == kCurrentDirectory || base == kParentDirectory) {
+      return FilePath();
+    }
+  }
+
+  StringType ext = Extension();
+  StringType ret = RemoveExtension().value();
+  ret.append(suffix);
+  ret.append(ext);
+  return FilePath(ret);
+}
+
+FilePath FilePath::ReplaceExtension(const StringType& extension) const {
+  if (path_.empty())
+    return FilePath();
+
+  StringType base = BaseName().value();
+  if (base.empty())
+    return FilePath();
+  if (*(base.end() - 1) == kExtensionSeparator) {
+    // Special case "." and ".."
+    if (base == kCurrentDirectory || base == kParentDirectory) {
+      return FilePath();
+    }
+  }
+
+  FilePath no_ext = RemoveExtension();
+  // If the new extension is "" or ".", then just remove the current extension.
+  if (extension.empty() || extension == StringType(1, kExtensionSeparator))
+    return no_ext;
+
+  StringType str = no_ext.value();
+  if (extension[0] != kExtensionSeparator)
+    str.append(1, kExtensionSeparator);
+  str.append(extension);
+  return FilePath(str);
+}
+
+FilePath FilePath::Append(const StringType& component) const {
   DCHECK(!IsPathAbsolute(component));
   if (path_.compare(kCurrentDirectory) == 0) {
     // Append normally doesn't do any normalization, but as a special case,
diff --git a/base/file_path.h b/base/file_path.h
index 6ed9320..d80298d 100644
--- a/base/file_path.h
+++ b/base/file_path.h
@@ -109,6 +109,9 @@
   // A special path component meaning "the parent directory."
   static const CharType kParentDirectory[];
 
+  // The character used to identify a file extension.
+  static const CharType kExtensionSeparator;
+
   FilePath() {}
   FilePath(const FilePath& that) : path_(that.path_) {}
   explicit FilePath(const StringType& path) : path_(path) {}
@@ -147,6 +150,36 @@
   // this is the only situation in which BaseName will return an absolute path.
   FilePath BaseName() const;
 
+  // Returns ".jpg" for path "C:\pics\jojo.jpg", or an empty string if
+  // the file has no extension.  If non-empty, Extension() will always start
+  // with precisely one ".".  The following code should always work regardless
+  // of the value of path.
+  // new_path = path.RemoveExtension().value().append(path.Extension());
+  // ASSERT(new_path == path.value());
+  // NOTE: this is different from the original file_util implementation which
+  // returned the extension without a leading "." ("jpg" instead of ".jpg")
+  StringType Extension() const;
+
+  // Returns "C:\pics\jojo" for path "C:\pics\jojo.jpg"
+  // NOTE: this is slightly different from the similar file_util implementation
+  // which returned simply 'jojo'.
+  FilePath RemoveExtension() const;
+
+  // Inserts |suffix| after the file name portion of |path| but before the
+  // extension.  Returns "" if BaseName() == "." or "..".
+  // Examples:
+  // path == "C:\pics\jojo.jpg" suffix == " (1)", returns "C:\pics\jojo (1).jpg"
+  // path == "jojo.jpg"         suffix == " (1)", returns "jojo (1).jpg"
+  // path == "C:\pics\jojo"     suffix == " (1)", returns "C:\pics\jojo (1)"
+  // path == "C:\pics.old\jojo" suffix == " (1)", returns "C:\pics.old\jojo (1)"
+  FilePath InsertBeforeExtension(const StringType& suffix) const;
+
+  // Replaces the extension of |file_name| with |extension|.  If |file_name|
+  // does not have an extension, them |extension| is added.  If |extension| is
+  // empty, then the extension is removed from |file_name|.
+  // Returns "" if BaseName() == "." or "..".
+  FilePath ReplaceExtension(const StringType& extension) const;
+
   // Returns a FilePath by appending a separator and the supplied path
   // component to this object's path.  Append takes care to avoid adding
   // excessive separators if this object's path already ends with a separator.
diff --git a/base/file_path_unittest.cc b/base/file_path_unittest.cc
index b4ad16f..ac45e46 100644
--- a/base/file_path_unittest.cc
+++ b/base/file_path_unittest.cc
@@ -406,3 +406,162 @@
 
   // Note: whether 
 }
+
+TEST_F(FilePathTest, Extension) {
+  FilePath base_dir(FILE_PATH_LITERAL("base_dir"));
+
+  FilePath jpg = base_dir.Append(FILE_PATH_LITERAL("foo.jpg"));
+  EXPECT_EQ(jpg.Extension(), FILE_PATH_LITERAL(".jpg"));
+
+  FilePath base = jpg.BaseName().RemoveExtension();
+  EXPECT_EQ(base.value(), FILE_PATH_LITERAL("foo"));
+
+  FilePath path_no_ext = base_dir.Append(base);
+  EXPECT_EQ(jpg.RemoveExtension().value(), path_no_ext.value());
+
+  EXPECT_EQ(path_no_ext.value(), path_no_ext.RemoveExtension().value());
+  EXPECT_EQ(path_no_ext.Extension(), FILE_PATH_LITERAL(""));
+}
+
+TEST_F(FilePathTest, Extension2) {
+  const struct UnaryTestData cases[] = {
+#if defined(FILE_PATH_USES_WIN_SEPARATORS)
+    { FPL("C:\\a\\b\\c.ext"),        FPL(".ext") },
+    { FPL("C:\\a\\b\\c."),           FPL(".") },
+    { FPL("C:\\a\\b\\c"),            FPL("") },
+    { FPL("C:\\a\\b\\"),             FPL("") },
+    { FPL("C:\\a\\b.\\"),            FPL(".") },
+    { FPL("C:\\a\\b\\c.ext1.ext2"),  FPL(".ext2") },
+    { FPL("C:\\foo.bar\\\\\\"),      FPL(".bar") },
+    { FPL("C:\\foo.bar\\.."),        FPL("") },
+    { FPL("C:\\foo.bar\\..\\\\"),    FPL("") },
+#endif
+    { FPL("/foo/bar/baz.ext"),       FPL(".ext") },
+    { FPL("/foo/bar/baz."),          FPL(".") },
+    { FPL("/foo/bar/baz.."),         FPL(".") },
+    { FPL("/foo/bar/baz"),           FPL("") },
+    { FPL("/foo/bar/"),              FPL("") },
+    { FPL("/foo/bar./"),             FPL(".") },
+    { FPL("/foo/bar/baz.ext1.ext2"), FPL(".ext2") },
+    { FPL("."),                      FPL("") },
+    { FPL(".."),                     FPL("") },
+    { FPL("./foo"),                  FPL("") },
+    { FPL("./foo.ext"),              FPL(".ext") },
+    { FPL("/foo.ext1/bar.ext2"),     FPL(".ext2") },
+    { FPL("/foo.bar////"),           FPL(".bar") },
+    { FPL("/foo.bar/.."),            FPL("") },
+    { FPL("/foo.bar/..////"),        FPL("") },
+  };
+  for (unsigned int i = 0; i < arraysize(cases); ++i) {
+    FilePath path(cases[i].input);
+    FilePath::StringType extension = path.Extension();
+    EXPECT_STREQ(cases[i].expected, extension.c_str()) << "i: " << i <<
+        ", path: " << path.value();
+  }
+}
+
+TEST_F(FilePathTest, InsertBeforeExtension) {
+  const struct BinaryTestData cases[] = {
+    { { FPL(""),                FPL("") },        FPL("") },
+    { { FPL(""),                FPL("txt") },     FPL("") },
+    { { FPL("."),               FPL("txt") },     FPL("") },
+    { { FPL(".."),              FPL("txt") },     FPL("") },
+    { { FPL("foo.dll"),         FPL("txt") },     FPL("footxt.dll") },
+    { { FPL("."),               FPL("") },        FPL(".") },
+    { { FPL("foo.dll"),         FPL(".txt") },    FPL("foo.txt.dll") },
+    { { FPL("foo"),             FPL("txt") },     FPL("footxt") },
+    { { FPL("foo"),             FPL(".txt") },    FPL("foo.txt") },
+    { { FPL("foo.baz.dll"),     FPL("txt") },     FPL("foo.baztxt.dll") },
+    { { FPL("foo.baz.dll"),     FPL(".txt") },    FPL("foo.baz.txt.dll") },
+    { { FPL("foo.dll"),         FPL("") },        FPL("foo.dll") },
+    { { FPL("foo.dll"),         FPL(".") },       FPL("foo..dll") },
+    { { FPL("foo"),             FPL("") },        FPL("foo") },
+    { { FPL("foo"),             FPL(".") },       FPL("foo.") },
+    { { FPL("foo.baz.dll"),     FPL("") },        FPL("foo.baz.dll") },
+    { { FPL("foo.baz.dll"),     FPL(".") },       FPL("foo.baz..dll") },
+#if defined(FILE_PATH_USES_WIN_SEPARATORS)
+    { { FPL("\\"),              FPL("") },        FPL("\\") },
+    { { FPL("\\"),              FPL("txt") },     FPL("\\txt") },
+    { { FPL("\\."),             FPL("txt") },     FPL("") },
+    { { FPL("\\.."),            FPL("txt") },     FPL("") },
+    { { FPL("\\."),             FPL("") },        FPL("\\.") },
+    { { FPL("C:\\bar\\foo.dll"), FPL("txt") },
+        FPL("C:\\bar\\footxt.dll") },
+    { { FPL("C:\\bar.baz\\foodll"), FPL("txt") },
+        FPL("C:\\bar.baz\\foodlltxt") },
+    { { FPL("C:\\bar.baz\\foo.dll"), FPL("txt") },
+        FPL("C:\\bar.baz\\footxt.dll") },
+    { { FPL("C:\\bar.baz\\foo.dll.exe"), FPL("txt") },
+        FPL("C:\\bar.baz\\foo.dlltxt.exe") },
+    { { FPL("C:\\bar.baz\\foo"), FPL("") },
+        FPL("C:\\bar.baz\\foo") },
+    { { FPL("C:\\bar.baz\\foo.exe"), FPL("") },
+        FPL("C:\\bar.baz\\foo.exe") },
+    { { FPL("C:\\bar.baz\\foo.dll.exe"), FPL("") },
+        FPL("C:\\bar.baz\\foo.dll.exe") },
+    { { FPL("C:\\bar\\baz\\foo.exe"), FPL(" (1)") },
+        FPL("C:\\bar\\baz\\foo (1).exe") },
+    { { FPL("C:\\foo.baz\\\\"), FPL(" (1)") },    FPL("C:\\foo (1).baz") },
+    { { FPL("C:\\foo.baz\\..\\"), FPL(" (1)") },  FPL("") },
+#endif
+    { { FPL("/"),               FPL("") },        FPL("/") },
+    { { FPL("/"),               FPL("txt") },     FPL("/txt") },
+    { { FPL("/."),              FPL("txt") },     FPL("") },
+    { { FPL("/.."),             FPL("txt") },     FPL("") },
+    { { FPL("/."),              FPL("") },        FPL("/.") },
+    { { FPL("/bar/foo.dll"),    FPL("txt") },     FPL("/bar/footxt.dll") },
+    { { FPL("/bar.baz/foodll"), FPL("txt") },     FPL("/bar.baz/foodlltxt") },
+    { { FPL("/bar.baz/foo.dll"), FPL("txt") },    FPL("/bar.baz/footxt.dll") },
+    { { FPL("/bar.baz/foo.dll.exe"), FPL("txt") },
+        FPL("/bar.baz/foo.dlltxt.exe") },
+    { { FPL("/bar.baz/foo"),    FPL("") },        FPL("/bar.baz/foo") },
+    { { FPL("/bar.baz/foo.exe"), FPL("") },       FPL("/bar.baz/foo.exe") },
+    { { FPL("/bar.baz/foo.dll.exe"), FPL("") },   FPL("/bar.baz/foo.dll.exe") },
+    { { FPL("/bar/baz/foo.exe"), FPL(" (1)") },   FPL("/bar/baz/foo (1).exe") },
+    { { FPL("/bar/baz/..////"), FPL(" (1)") },    FPL("") },
+  };
+  for (unsigned int i = 0; i < arraysize(cases); ++i) {
+    FilePath path(cases[i].inputs[0]);
+    FilePath result = path.InsertBeforeExtension(cases[i].inputs[1]);
+    EXPECT_EQ(cases[i].expected, result.value()) << "i: " << i <<
+        ", path: " << path.value() << ", insert: " << cases[i].inputs[1];
+  }
+}
+
+TEST_F(FilePathTest, ReplaceExtension) {
+  const struct BinaryTestData cases[] = {
+    { { FPL(""),              FPL("") },      FPL("") },
+    { { FPL(""),              FPL("txt") },   FPL("") },
+    { { FPL("."),             FPL("txt") },   FPL("") },
+    { { FPL(".."),            FPL("txt") },   FPL("") },
+    { { FPL("."),             FPL("") },      FPL("") },
+    { { FPL("foo.dll"),       FPL("txt") },   FPL("foo.txt") },
+    { { FPL("foo..dll"),      FPL("txt") },   FPL("foo..txt") },
+    { { FPL("foo.dll"),       FPL(".txt") },  FPL("foo.txt") },
+    { { FPL("foo"),           FPL("txt") },   FPL("foo.txt") },
+    { { FPL("foo."),          FPL("txt") },   FPL("foo.txt") },
+    { { FPL("foo.."),         FPL("txt") },   FPL("foo..txt") },
+    { { FPL("foo"),           FPL(".txt") },  FPL("foo.txt") },
+    { { FPL("foo.baz.dll"),   FPL("txt") },   FPL("foo.baz.txt") },
+    { { FPL("foo.baz.dll"),   FPL(".txt") },  FPL("foo.baz.txt") },
+    { { FPL("foo.dll"),       FPL("") },      FPL("foo") },
+    { { FPL("foo.dll"),       FPL(".") },     FPL("foo") },
+    { { FPL("foo"),           FPL("") },      FPL("foo") },
+    { { FPL("foo"),           FPL(".") },     FPL("foo") },
+    { { FPL("foo.baz.dll"),   FPL("") },      FPL("foo.baz") },
+    { { FPL("foo.baz.dll"),   FPL(".") },     FPL("foo.baz") },
+#if defined(FILE_PATH_USES_WIN_SEPARATORS)
+    { { FPL("C:\\foo.bar\\foo"),    FPL("baz") }, FPL("C:\\foo.bar\\foo.baz") },
+    { { FPL("C:\\foo.bar\\..\\\\"), FPL("baz") }, FPL("") },
+#endif
+    { { FPL("/foo.bar/foo"),        FPL("baz") }, FPL("/foo.bar/foo.baz") },
+    { { FPL("/foo.bar/..////"),     FPL("baz") }, FPL("") },
+  };
+  for (unsigned int i = 0; i < arraysize(cases); ++i) {
+    FilePath path(cases[i].inputs[0]);
+    FilePath replaced = path.ReplaceExtension(cases[i].inputs[1]);
+    EXPECT_EQ(cases[i].expected, replaced.value()) << "i: " << i <<
+        ", path: " << path.value() << ", replace: " << cases[i].inputs[1];
+  }
+}
+
diff --git a/base/file_util.h b/base/file_util.h
index 784ea36..c064b2c 100644
--- a/base/file_util.h
+++ b/base/file_util.h
@@ -75,13 +75,12 @@
 // Deprecated. Use FilePath::BaseName instead.
 std::wstring GetFilenameFromPath(const std::wstring& path);
 
-// Returns "jpg" for path "C:\pics\jojo.jpg", or an empty string if
-// the file has no extension.
+// Deprecated compatibility function.  Use FilePath::Extension.
 FilePath::StringType GetFileExtensionFromPath(const FilePath& path);
 // Deprecated temporary compatibility function.
 std::wstring GetFileExtensionFromPath(const std::wstring& path);
 
-// Returns 'jojo' for path "C:\pics\jojo.jpg".
+// Deprecated compatibility function.  Use FilePath::RemoveExtension.
 std::wstring GetFilenameWithoutExtensionFromPath(const std::wstring& path);
 
 // Returns the directory component of a path, without the trailing
@@ -104,18 +103,15 @@
 // Deprecated temporary compatibility function.
 bool AbsolutePath(std::wstring* path);
 
-// Inserts |suffix| after the file name portion of |path| but before the
-// extension.
-// Examples:
-// path == "C:\pics\jojo.jpg" suffix == " (1)", returns "C:\pics\jojo (1).jpg"
-// path == "jojo.jpg"         suffix == " (1)", returns "jojo (1).jpg"
-// path == "C:\pics\jojo"     suffix == " (1)", returns "C:\pics\jojo (1)"
-// path == "C:\pics.old\jojo" suffix == " (1)", returns "C:\pics.old\jojo (1)"
+// Returns true if this FilePath represents a parent dir of |other|. Both
+// paths are normalized before doing the comparison, but neither |this| nor
+// |other| are modified.
+bool PathContains(const FilePath& path, const FilePath& other);
+
+// Deprecated compatibility function.  Use FilePath::InsertBeforeExtension.
 void InsertBeforeExtension(FilePath* path, const FilePath::StringType& suffix);
 
-// Replaces the extension of |file_name| with |extension|.  If |file_name|
-// does not have an extension, them |extension| is added.  If |extension| is
-// empty, then the extension is removed from |file_name|.
+// Deprecated compatibility function.  Use FilePath::ReplaceExtension.
 void ReplaceExtension(FilePath* file_name,
                       const FilePath::StringType& extension);