LfsPointerFileValidator.cpp 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include <native/AssetManager/Validators/LfsPointerFileValidator.h>
  9. #include <native/assetprocessor.h>
  10. #include <AzFramework/FileFunc/FileFunc.h>
  11. #include <AzCore/Settings/SettingsRegistryMergeUtils.h>
  12. namespace AssetProcessor
  13. {
  14. LfsPointerFileValidator::LfsPointerFileValidator(const AZStd::vector<AZStd::string>& scanDirectories)
  15. {
  16. for (const AZStd::string& directory : scanDirectories)
  17. {
  18. ParseGitAttributesFile(directory.c_str());
  19. }
  20. }
  21. void LfsPointerFileValidator::ParseGitAttributesFile(const AZStd::string& directory)
  22. {
  23. constexpr const char* gitAttributesFileName = ".gitattributes";
  24. AZStd::string gitAttributesFilePath = AZStd::string::format("%s/%s", directory.c_str(), gitAttributesFileName);
  25. if (!AzFramework::StringFunc::Path::Normalize(gitAttributesFilePath))
  26. {
  27. AZ_Error(AssetProcessor::DebugChannel, false,
  28. "Failed to normalize %s file path %s.", gitAttributesFileName, gitAttributesFilePath.c_str());
  29. }
  30. if (!AZ::IO::FileIOBase::GetInstance()->Exists(gitAttributesFilePath.c_str()))
  31. {
  32. return;
  33. }
  34. // A gitattributes file is a simple text file that gives attributes to pathnames.
  35. // Each line in gitattributes file is of form: pattern attr1 attr2 ...
  36. // Example for LFS pointer file attributes: *.DLL filter=lfs diff=lfs merge=lfs -text
  37. auto result = AzFramework::FileFunc::ReadTextFileByLine(gitAttributesFilePath, [this](const char* line) -> bool
  38. {
  39. // Skip any empty or comment lines
  40. if (strlen(line) && line[0] != '#')
  41. {
  42. AZStd::regex lineRegex("^([^ ]+) filter=lfs diff=lfs merge=lfs -text\\n$");
  43. AZStd::cmatch matchResult;
  44. if (AZStd::regex_search(line, matchResult, lineRegex) && matchResult.size() == 2)
  45. {
  46. // The current line matches the LFS attributes format. Record the LFS pointer file path pattern.
  47. m_lfsPointerFilePatterns.insert(matchResult[1]);
  48. }
  49. }
  50. return true;
  51. });
  52. AZ_Error(AssetProcessor::DebugChannel, result.IsSuccess(), result.GetError().c_str());
  53. }
  54. bool LfsPointerFileValidator::IsLfsPointerFile(const AZStd::string& filePath)
  55. {
  56. return AZ::IO::FileIOBase::GetInstance()->Exists(filePath.c_str()) &&
  57. CheckLfsPointerFilePathPattern(filePath) &&
  58. CheckLfsPointerFileContent(filePath);
  59. }
  60. AZStd::set<AZStd::string> LfsPointerFileValidator::GetLfsPointerFilePathPatterns()
  61. {
  62. return m_lfsPointerFilePatterns;
  63. }
  64. bool LfsPointerFileValidator::CheckLfsPointerFilePathPattern(const AZStd::string& filePath)
  65. {
  66. bool matches = false;
  67. for (const AZStd::string& pattern : GetLfsPointerFilePathPatterns())
  68. {
  69. if (AZStd::wildcard_match(pattern, filePath.c_str()))
  70. {
  71. // The file path matches one of the known LFS pointer file path patterns.
  72. matches = true;
  73. break;
  74. }
  75. }
  76. return matches;
  77. }
  78. bool LfsPointerFileValidator::CheckLfsPointerFileContent(const AZStd::string& filePath)
  79. {
  80. // Content rules for LFS pointer files (https://github.com/git-lfs/git-lfs/blob/main/docs/spec.md):
  81. // 1. Pointer files are text files which MUST contain only UTF-8 characters.
  82. // 2. Each line MUST be of the format{key} {value}\n(trailing unix newline). The required keys are: "version", "oid" and "size".
  83. // 3. Only a single space character between {key} and {value}.
  84. // 4. Keys MUST only use the characters [a-z] [0-9] . -.
  85. // 5. The first key is always version.
  86. // 6. Lines of key/value pairs MUST be sorted alphabetically in ascending order (with the exception of version, which is always first).
  87. // 7. Values MUST NOT contain return or newline characters.
  88. // 8. Pointer files MUST be stored in Git with their executable bit matching that of the replaced file.
  89. // 9. Pointer files are unique: that is, there is exactly one valid encoding for a pointer file.
  90. AZStd::vector<AZStd::string> fileKeys;
  91. bool contentCheckSucceeded = true;
  92. auto result = AzFramework::FileFunc::ReadTextFileByLine(filePath,
  93. [&fileKeys, &contentCheckSucceeded](const char* line) -> bool
  94. {
  95. constexpr const char* lfsVersionKey = "version";
  96. AZStd::regex lineRegex("^([a-z0-9\\.-]+) ([^\\r\\n]+)\\n$");
  97. AZStd::cmatch matchResult;
  98. if (!AZStd::regex_search(line, matchResult, lineRegex) ||
  99. matchResult.size() <= 2 ||
  100. (fileKeys.size() == 0 && matchResult[1] != lfsVersionKey) ||
  101. (fileKeys.size() > 1 && matchResult[1] < fileKeys[fileKeys.size() - 1]))
  102. {
  103. // The current line doesn't match the LFS pointer file content rules above.
  104. // Return early in this case since the file is not an LFS content file.
  105. contentCheckSucceeded = false;
  106. return false;
  107. }
  108. fileKeys.emplace_back(matchResult[1]);
  109. return true;
  110. });
  111. contentCheckSucceeded &= result.IsSuccess();
  112. if (contentCheckSucceeded)
  113. {
  114. // Check whether all the required keys exist in the LFS pointer file.
  115. const AZStd::vector<AZStd::string> RequiredKeys = { "version", "oid", "size" };
  116. size_t requiredKeyIndex = 0, fileKeyIndex = 0;
  117. while (requiredKeyIndex < RequiredKeys.size() && fileKeyIndex < fileKeys.size())
  118. {
  119. if (RequiredKeys[requiredKeyIndex] == fileKeys[fileKeyIndex])
  120. {
  121. ++requiredKeyIndex;
  122. }
  123. ++fileKeyIndex;
  124. }
  125. contentCheckSucceeded &= (requiredKeyIndex == RequiredKeys.size());
  126. }
  127. return contentCheckSucceeded;
  128. }
  129. }