| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 1 | // | 
 | 2 | // Copyright (C) 2017 The Android Open Source Project | 
 | 3 | // | 
 | 4 | // Licensed under the Apache License, Version 2.0 (the "License"); | 
 | 5 | // you may not use this file except in compliance with the License. | 
 | 6 | // You may obtain a copy of the License at | 
 | 7 | // | 
 | 8 | //      http://www.apache.org/licenses/LICENSE-2.0 | 
 | 9 | // | 
 | 10 | // Unless required by applicable law or agreed to in writing, software | 
 | 11 | // distributed under the License is distributed on an "AS IS" BASIS, | 
 | 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
 | 13 | // See the License for the specific language governing permissions and | 
 | 14 | // limitations under the License. | 
 | 15 | // | 
 | 16 |  | 
 | 17 | #include "update_engine/payload_generator/squashfs_filesystem.h" | 
 | 18 |  | 
 | 19 | #include <fcntl.h> | 
 | 20 |  | 
 | 21 | #include <algorithm> | 
 | 22 | #include <string> | 
| Amin Hassani | 3cd4df1 | 2017-08-25 11:21:53 -0700 | [diff] [blame] | 23 | #include <utility> | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 24 |  | 
 | 25 | #include <base/files/file_util.h> | 
| Amin Hassani | 77c25fc | 2019-01-29 10:24:19 -0800 | [diff] [blame] | 26 | #include <base/files/scoped_temp_dir.h> | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 27 | #include <base/logging.h> | 
 | 28 | #include <base/strings/string_number_conversions.h> | 
 | 29 | #include <base/strings/string_split.h> | 
 | 30 | #include <brillo/streams/file_stream.h> | 
 | 31 |  | 
 | 32 | #include "update_engine/common/subprocess.h" | 
 | 33 | #include "update_engine/common/utils.h" | 
| Amin Hassani | 3cd4df1 | 2017-08-25 11:21:53 -0700 | [diff] [blame] | 34 | #include "update_engine/payload_generator/deflate_utils.h" | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 35 | #include "update_engine/payload_generator/delta_diff_generator.h" | 
 | 36 | #include "update_engine/payload_generator/extent_ranges.h" | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 37 | #include "update_engine/update_metadata.pb.h" | 
 | 38 |  | 
| Amin Hassani | 77c25fc | 2019-01-29 10:24:19 -0800 | [diff] [blame] | 39 | using base::FilePath; | 
 | 40 | using base::ScopedTempDir; | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 41 | using std::string; | 
 | 42 | using std::unique_ptr; | 
 | 43 | using std::vector; | 
 | 44 |  | 
 | 45 | namespace chromeos_update_engine { | 
 | 46 |  | 
 | 47 | namespace { | 
 | 48 |  | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 49 | // The size of the squashfs super block. | 
 | 50 | constexpr size_t kSquashfsSuperBlockSize = 96; | 
 | 51 | constexpr uint64_t kSquashfsCompressedBit = 1 << 24; | 
| Amin Hassani | 3cd4df1 | 2017-08-25 11:21:53 -0700 | [diff] [blame] | 52 | constexpr uint32_t kSquashfsZlibCompression = 1; | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 53 |  | 
 | 54 | bool ReadSquashfsHeader(const brillo::Blob blob, | 
 | 55 |                         SquashfsFilesystem::SquashfsHeader* header) { | 
 | 56 |   if (blob.size() < kSquashfsSuperBlockSize) { | 
 | 57 |     return false; | 
 | 58 |   } | 
 | 59 |  | 
 | 60 |   memcpy(&header->magic, blob.data(), 4); | 
 | 61 |   memcpy(&header->block_size, blob.data() + 12, 4); | 
 | 62 |   memcpy(&header->compression_type, blob.data() + 20, 2); | 
 | 63 |   memcpy(&header->major_version, blob.data() + 28, 2); | 
 | 64 |   return true; | 
 | 65 | } | 
 | 66 |  | 
 | 67 | bool CheckHeader(const SquashfsFilesystem::SquashfsHeader& header) { | 
 | 68 |   return header.magic == 0x73717368 && header.major_version == 4; | 
 | 69 | } | 
 | 70 |  | 
 | 71 | bool GetFileMapContent(const string& sqfs_path, string* map) { | 
| Amin Hassani | ed03b44 | 2020-10-26 17:21:29 -0700 | [diff] [blame] | 72 |   ScopedTempFile map_file("squashfs_file_map.XXXXXX"); | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 73 |   // Run unsquashfs to get the system file map. | 
 | 74 |   // unsquashfs -m <map-file> <squashfs-file> | 
| Kelvin Zhang | f68e825 | 2023-06-13 11:16:16 -0700 | [diff] [blame] | 75 |   const char* unsquashfs = getenv("UNSQUASHFS"); | 
 | 76 |   if (unsquashfs == nullptr || unsquashfs[0] == '\0') { | 
 | 77 |     unsquashfs = "unsquashfs"; | 
 | 78 |   } | 
 | 79 |   vector<string> cmd = {unsquashfs, "-m", map_file.path(), sqfs_path}; | 
| Colin Cross | d76a8ac | 2021-12-21 13:08:20 -0800 | [diff] [blame] | 80 |   string stdout_str, stderr_str; | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 81 |   int exit_code; | 
| Colin Cross | d76a8ac | 2021-12-21 13:08:20 -0800 | [diff] [blame] | 82 |   if (!Subprocess::SynchronousExec(cmd, &exit_code, &stdout_str, &stderr_str) || | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 83 |       exit_code != 0) { | 
| Amin Hassani | 3a4caa1 | 2019-11-06 11:12:28 -0800 | [diff] [blame] | 84 |     LOG(ERROR) << "Failed to run `unsquashfs -m` with stdout content: " | 
| Colin Cross | d76a8ac | 2021-12-21 13:08:20 -0800 | [diff] [blame] | 85 |                << stdout_str << " and stderr content: " << stderr_str; | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 86 |     return false; | 
 | 87 |   } | 
| Amin Hassani | ed03b44 | 2020-10-26 17:21:29 -0700 | [diff] [blame] | 88 |   TEST_AND_RETURN_FALSE(utils::ReadFile(map_file.path(), map)); | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 89 |   return true; | 
 | 90 | } | 
 | 91 |  | 
 | 92 | }  // namespace | 
 | 93 |  | 
 | 94 | bool SquashfsFilesystem::Init(const string& map, | 
| Amin Hassani | 3cd4df1 | 2017-08-25 11:21:53 -0700 | [diff] [blame] | 95 |                               const string& sqfs_path, | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 96 |                               size_t size, | 
| Amin Hassani | 3cd4df1 | 2017-08-25 11:21:53 -0700 | [diff] [blame] | 97 |                               const SquashfsHeader& header, | 
 | 98 |                               bool extract_deflates) { | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 99 |   size_ = size; | 
| Amin Hassani | 3cd4df1 | 2017-08-25 11:21:53 -0700 | [diff] [blame] | 100 |  | 
 | 101 |   bool is_zlib = header.compression_type == kSquashfsZlibCompression; | 
 | 102 |   if (!is_zlib) { | 
 | 103 |     LOG(WARNING) << "Filesystem is not Gzipped. Not filling deflates!"; | 
 | 104 |   } | 
 | 105 |   vector<puffin::ByteExtent> zlib_blks; | 
 | 106 |  | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 107 |   // Reading files map. For the format of the file map look at the comments for | 
 | 108 |   // |CreateFromFileMap()|. | 
 | 109 |   auto lines = base::SplitStringPiece(map, | 
 | 110 |                                       "\n", | 
 | 111 |                                       base::WhitespaceHandling::KEEP_WHITESPACE, | 
 | 112 |                                       base::SplitResult::SPLIT_WANT_NONEMPTY); | 
 | 113 |   for (const auto& line : lines) { | 
 | 114 |     auto splits = | 
 | 115 |         base::SplitStringPiece(line, | 
 | 116 |                                " \t", | 
 | 117 |                                base::WhitespaceHandling::TRIM_WHITESPACE, | 
 | 118 |                                base::SplitResult::SPLIT_WANT_NONEMPTY); | 
 | 119 |     // Only filename is invalid. | 
 | 120 |     TEST_AND_RETURN_FALSE(splits.size() > 1); | 
 | 121 |     uint64_t start; | 
 | 122 |     TEST_AND_RETURN_FALSE(base::StringToUint64(splits[1], &start)); | 
 | 123 |     uint64_t cur_offset = start; | 
| Amin Hassani | 1a200c1 | 2020-02-26 14:47:23 -0800 | [diff] [blame] | 124 |     bool is_compressed = false; | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 125 |     for (size_t i = 2; i < splits.size(); ++i) { | 
 | 126 |       uint64_t blk_size; | 
 | 127 |       TEST_AND_RETURN_FALSE(base::StringToUint64(splits[i], &blk_size)); | 
 | 128 |       // TODO(ahassani): For puffin push it into a proper list if uncompressed. | 
 | 129 |       auto new_blk_size = blk_size & ~kSquashfsCompressedBit; | 
 | 130 |       TEST_AND_RETURN_FALSE(new_blk_size <= header.block_size); | 
| Amin Hassani | 3cd4df1 | 2017-08-25 11:21:53 -0700 | [diff] [blame] | 131 |       if (new_blk_size > 0 && !(blk_size & kSquashfsCompressedBit)) { | 
| Amin Hassani | 1a200c1 | 2020-02-26 14:47:23 -0800 | [diff] [blame] | 132 |         // It is a compressed block. | 
| Amin Hassani | 3cd4df1 | 2017-08-25 11:21:53 -0700 | [diff] [blame] | 133 |         if (is_zlib && extract_deflates) { | 
 | 134 |           zlib_blks.emplace_back(cur_offset, new_blk_size); | 
 | 135 |         } | 
| Amin Hassani | 1a200c1 | 2020-02-26 14:47:23 -0800 | [diff] [blame] | 136 |         is_compressed = true; | 
| Amin Hassani | 3cd4df1 | 2017-08-25 11:21:53 -0700 | [diff] [blame] | 137 |       } | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 138 |       cur_offset += new_blk_size; | 
 | 139 |     } | 
 | 140 |  | 
 | 141 |     // If size is zero do not add the file. | 
 | 142 |     if (cur_offset - start > 0) { | 
 | 143 |       File file; | 
 | 144 |       file.name = splits[0].as_string(); | 
 | 145 |       file.extents = {ExtentForBytes(kBlockSize, start, cur_offset - start)}; | 
| Amin Hassani | 1a200c1 | 2020-02-26 14:47:23 -0800 | [diff] [blame] | 146 |       file.is_compressed = is_compressed; | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 147 |       files_.emplace_back(file); | 
 | 148 |     } | 
 | 149 |   } | 
 | 150 |  | 
 | 151 |   // Sort all files by their offset in the squashfs. | 
 | 152 |   std::sort(files_.begin(), files_.end(), [](const File& a, const File& b) { | 
 | 153 |     return a.extents[0].start_block() < b.extents[0].start_block(); | 
 | 154 |   }); | 
 | 155 |   // If there is any overlap between two consecutive extents, remove them. Here | 
 | 156 |   // we are assuming all files have exactly one extent. If this assumption | 
 | 157 |   // changes then this implementation needs to change too. | 
| Jae Hoon Kim | 3f894a8 | 2020-05-20 19:26:19 -0700 | [diff] [blame] | 158 |   for (auto first = files_.begin(), | 
 | 159 |             second = first + (first == files_.end() ? 0 : 1); | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 160 |        first != files_.end() && second != files_.end(); | 
 | 161 |        second = first + 1) { | 
 | 162 |     auto first_begin = first->extents[0].start_block(); | 
 | 163 |     auto first_end = first_begin + first->extents[0].num_blocks(); | 
 | 164 |     auto second_begin = second->extents[0].start_block(); | 
 | 165 |     auto second_end = second_begin + second->extents[0].num_blocks(); | 
 | 166 |     // Remove the first file if the size is zero. | 
 | 167 |     if (first_end == first_begin) { | 
 | 168 |       first = files_.erase(first); | 
 | 169 |     } else if (first_end > second_begin) {  // We found a collision. | 
 | 170 |       if (second_end <= first_end) { | 
 | 171 |         // Second file is inside the first file, remove the second file. | 
 | 172 |         second = files_.erase(second); | 
 | 173 |       } else if (first_begin == second_begin) { | 
 | 174 |         // First file is inside the second file, remove the first file. | 
 | 175 |         first = files_.erase(first); | 
 | 176 |       } else { | 
 | 177 |         // Remove overlapping extents from the first file. | 
 | 178 |         first->extents[0].set_num_blocks(second_begin - first_begin); | 
 | 179 |         ++first; | 
 | 180 |       } | 
 | 181 |     } else { | 
 | 182 |       ++first; | 
 | 183 |     } | 
 | 184 |   } | 
 | 185 |  | 
 | 186 |   // Find all the metadata including superblock and add them to the list of | 
 | 187 |   // files. | 
 | 188 |   ExtentRanges file_extents; | 
 | 189 |   for (const auto& file : files_) { | 
 | 190 |     file_extents.AddExtents(file.extents); | 
 | 191 |   } | 
| Sen Jiang | 0a582fb | 2018-06-26 19:27:21 -0700 | [diff] [blame] | 192 |   vector<Extent> full = {ExtentForBytes(kBlockSize, 0, size_)}; | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 193 |   auto metadata_extents = FilterExtentRanges(full, file_extents); | 
 | 194 |   // For now there should be at most two extents. One for superblock and one for | 
 | 195 |   // metadata at the end. Just create appropriate files with <metadata-i> name. | 
 | 196 |   // We can add all these extents as one metadata too, but that violates the | 
 | 197 |   // contiguous write optimization. | 
 | 198 |   for (size_t i = 0; i < metadata_extents.size(); i++) { | 
 | 199 |     File file; | 
 | 200 |     file.name = "<metadata-" + std::to_string(i) + ">"; | 
 | 201 |     file.extents = {metadata_extents[i]}; | 
 | 202 |     files_.emplace_back(file); | 
 | 203 |   } | 
 | 204 |  | 
 | 205 |   // Do one last sort before returning. | 
 | 206 |   std::sort(files_.begin(), files_.end(), [](const File& a, const File& b) { | 
 | 207 |     return a.extents[0].start_block() < b.extents[0].start_block(); | 
 | 208 |   }); | 
| Amin Hassani | 3cd4df1 | 2017-08-25 11:21:53 -0700 | [diff] [blame] | 209 |  | 
 | 210 |   if (is_zlib && extract_deflates) { | 
 | 211 |     // If it is infact gzipped, then the sqfs_path should be valid to read its | 
 | 212 |     // content. | 
 | 213 |     TEST_AND_RETURN_FALSE(!sqfs_path.empty()); | 
 | 214 |     if (zlib_blks.empty()) { | 
 | 215 |       return true; | 
 | 216 |     } | 
 | 217 |  | 
 | 218 |     // Sort zlib blocks. | 
 | 219 |     std::sort(zlib_blks.begin(), | 
 | 220 |               zlib_blks.end(), | 
 | 221 |               [](const puffin::ByteExtent& a, const puffin::ByteExtent& b) { | 
 | 222 |                 return a.offset < b.offset; | 
 | 223 |               }); | 
 | 224 |  | 
| Amin Hassani | 5d18505 | 2019-04-23 07:28:30 -0700 | [diff] [blame] | 225 |     // Sometimes a squashfs can have a two files that are hard linked. In this | 
 | 226 |     // case both files will have the same starting offset in the image and hence | 
 | 227 |     // the same zlib blocks. So we need to remove these duplicates to eliminate | 
 | 228 |     // further potential probems. As a matter of fact the next statement will | 
 | 229 |     // fail if there are duplicates (there will be overlap between two blocks). | 
 | 230 |     auto last = std::unique(zlib_blks.begin(), zlib_blks.end()); | 
 | 231 |     zlib_blks.erase(last, zlib_blks.end()); | 
 | 232 |  | 
| Tianjie | e283ce4 | 2020-07-29 11:37:51 -0700 | [diff] [blame] | 233 |     // Make sure zlib blocks are not overlapping. | 
| Amin Hassani | 3cd4df1 | 2017-08-25 11:21:53 -0700 | [diff] [blame] | 234 |     auto result = std::adjacent_find( | 
 | 235 |         zlib_blks.begin(), | 
 | 236 |         zlib_blks.end(), | 
 | 237 |         [](const puffin::ByteExtent& a, const puffin::ByteExtent& b) { | 
 | 238 |           return (a.offset + a.length) > b.offset; | 
 | 239 |         }); | 
 | 240 |     TEST_AND_RETURN_FALSE(result == zlib_blks.end()); | 
 | 241 |  | 
 | 242 |     vector<puffin::BitExtent> deflates; | 
 | 243 |     TEST_AND_RETURN_FALSE( | 
 | 244 |         puffin::LocateDeflatesInZlibBlocks(sqfs_path, zlib_blks, &deflates)); | 
 | 245 |  | 
 | 246 |     // Add deflates for each file. | 
 | 247 |     for (auto& file : files_) { | 
 | 248 |       file.deflates = deflate_utils::FindDeflates(file.extents, deflates); | 
 | 249 |     } | 
 | 250 |   } | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 251 |   return true; | 
 | 252 | } | 
 | 253 |  | 
 | 254 | unique_ptr<SquashfsFilesystem> SquashfsFilesystem::CreateFromFile( | 
| Kelvin Zhang | ed9b208 | 2023-05-25 13:58:19 -0700 | [diff] [blame] | 255 |     const string& sqfs_path, bool extract_deflates) { | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 256 |   if (sqfs_path.empty()) | 
 | 257 |     return nullptr; | 
 | 258 |  | 
 | 259 |   brillo::StreamPtr sqfs_file = | 
| Amin Hassani | 77c25fc | 2019-01-29 10:24:19 -0800 | [diff] [blame] | 260 |       brillo::FileStream::Open(FilePath(sqfs_path), | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 261 |                                brillo::Stream::AccessMode::READ, | 
 | 262 |                                brillo::FileStream::Disposition::OPEN_EXISTING, | 
 | 263 |                                nullptr); | 
 | 264 |   if (!sqfs_file) { | 
 | 265 |     LOG(ERROR) << "Unable to open " << sqfs_path << " for reading."; | 
 | 266 |     return nullptr; | 
 | 267 |   } | 
 | 268 |  | 
 | 269 |   SquashfsHeader header; | 
 | 270 |   brillo::Blob blob(kSquashfsSuperBlockSize); | 
 | 271 |   if (!sqfs_file->ReadAllBlocking(blob.data(), blob.size(), nullptr)) { | 
 | 272 |     LOG(ERROR) << "Unable to read from file: " << sqfs_path; | 
 | 273 |     return nullptr; | 
 | 274 |   } | 
 | 275 |   if (!ReadSquashfsHeader(blob, &header) || !CheckHeader(header)) { | 
 | 276 |     // This is not necessary an error. | 
 | 277 |     return nullptr; | 
 | 278 |   } | 
 | 279 |  | 
 | 280 |   // Read the map file. | 
 | 281 |   string filemap; | 
 | 282 |   if (!GetFileMapContent(sqfs_path, &filemap)) { | 
 | 283 |     LOG(ERROR) << "Failed to produce squashfs map file: " << sqfs_path; | 
 | 284 |     return nullptr; | 
 | 285 |   } | 
 | 286 |  | 
 | 287 |   unique_ptr<SquashfsFilesystem> sqfs(new SquashfsFilesystem()); | 
| Amin Hassani | 3cd4df1 | 2017-08-25 11:21:53 -0700 | [diff] [blame] | 288 |   if (!sqfs->Init( | 
 | 289 |           filemap, sqfs_path, sqfs_file->GetSize(), header, extract_deflates)) { | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 290 |     LOG(ERROR) << "Failed to initialized the Squashfs file system"; | 
 | 291 |     return nullptr; | 
 | 292 |   } | 
| Amin Hassani | 3cd4df1 | 2017-08-25 11:21:53 -0700 | [diff] [blame] | 293 |  | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 294 |   return sqfs; | 
 | 295 | } | 
 | 296 |  | 
 | 297 | unique_ptr<SquashfsFilesystem> SquashfsFilesystem::CreateFromFileMap( | 
 | 298 |     const string& filemap, size_t size, const SquashfsHeader& header) { | 
 | 299 |   if (!CheckHeader(header)) { | 
 | 300 |     LOG(ERROR) << "Invalid Squashfs super block!"; | 
 | 301 |     return nullptr; | 
 | 302 |   } | 
 | 303 |  | 
 | 304 |   unique_ptr<SquashfsFilesystem> sqfs(new SquashfsFilesystem()); | 
| Amin Hassani | 3cd4df1 | 2017-08-25 11:21:53 -0700 | [diff] [blame] | 305 |   if (!sqfs->Init(filemap, "", size, header, false)) { | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 306 |     LOG(ERROR) << "Failed to initialize the Squashfs file system using filemap"; | 
 | 307 |     return nullptr; | 
 | 308 |   } | 
 | 309 |   // TODO(ahassani): Add a function that initializes the puffin related extents. | 
 | 310 |   return sqfs; | 
 | 311 | } | 
 | 312 |  | 
 | 313 | size_t SquashfsFilesystem::GetBlockSize() const { | 
 | 314 |   return kBlockSize; | 
 | 315 | } | 
 | 316 |  | 
 | 317 | size_t SquashfsFilesystem::GetBlockCount() const { | 
 | 318 |   return size_ / kBlockSize; | 
 | 319 | } | 
 | 320 |  | 
 | 321 | bool SquashfsFilesystem::GetFiles(vector<File>* files) const { | 
 | 322 |   files->insert(files->end(), files_.begin(), files_.end()); | 
 | 323 |   return true; | 
 | 324 | } | 
 | 325 |  | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 326 | bool SquashfsFilesystem::IsSquashfsImage(const brillo::Blob& blob) { | 
 | 327 |   SquashfsHeader header; | 
 | 328 |   return ReadSquashfsHeader(blob, &header) && CheckHeader(header); | 
 | 329 | } | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 330 | }  // namespace chromeos_update_engine |