| 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> | 
 | 26 | #include <base/logging.h> | 
 | 27 | #include <base/strings/string_number_conversions.h> | 
 | 28 | #include <base/strings/string_split.h> | 
 | 29 | #include <brillo/streams/file_stream.h> | 
 | 30 |  | 
 | 31 | #include "update_engine/common/subprocess.h" | 
 | 32 | #include "update_engine/common/utils.h" | 
| Amin Hassani | 3cd4df1 | 2017-08-25 11:21:53 -0700 | [diff] [blame] | 33 | #include "update_engine/payload_generator/deflate_utils.h" | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 34 | #include "update_engine/payload_generator/delta_diff_generator.h" | 
 | 35 | #include "update_engine/payload_generator/extent_ranges.h" | 
 | 36 | #include "update_engine/payload_generator/extent_utils.h" | 
 | 37 | #include "update_engine/update_metadata.pb.h" | 
 | 38 |  | 
 | 39 | using std::string; | 
 | 40 | using std::unique_ptr; | 
 | 41 | using std::vector; | 
 | 42 |  | 
 | 43 | namespace chromeos_update_engine { | 
 | 44 |  | 
 | 45 | namespace { | 
 | 46 |  | 
 | 47 | Extent ExtentForBytes(uint64_t block_size, | 
 | 48 |                       uint64_t start_bytes, | 
 | 49 |                       uint64_t size_bytes) { | 
 | 50 |   uint64_t start_block = start_bytes / block_size; | 
 | 51 |   uint64_t end_block = (start_bytes + size_bytes + block_size - 1) / block_size; | 
 | 52 |   return ExtentForRange(start_block, end_block - start_block); | 
 | 53 | } | 
 | 54 |  | 
 | 55 | // The size of the squashfs super block. | 
 | 56 | constexpr size_t kSquashfsSuperBlockSize = 96; | 
 | 57 | constexpr uint64_t kSquashfsCompressedBit = 1 << 24; | 
| Amin Hassani | 3cd4df1 | 2017-08-25 11:21:53 -0700 | [diff] [blame] | 58 | constexpr uint32_t kSquashfsZlibCompression = 1; | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 59 |  | 
 | 60 | bool ReadSquashfsHeader(const brillo::Blob blob, | 
 | 61 |                         SquashfsFilesystem::SquashfsHeader* header) { | 
 | 62 |   if (blob.size() < kSquashfsSuperBlockSize) { | 
 | 63 |     return false; | 
 | 64 |   } | 
 | 65 |  | 
 | 66 |   memcpy(&header->magic, blob.data(), 4); | 
 | 67 |   memcpy(&header->block_size, blob.data() + 12, 4); | 
 | 68 |   memcpy(&header->compression_type, blob.data() + 20, 2); | 
 | 69 |   memcpy(&header->major_version, blob.data() + 28, 2); | 
 | 70 |   return true; | 
 | 71 | } | 
 | 72 |  | 
 | 73 | bool CheckHeader(const SquashfsFilesystem::SquashfsHeader& header) { | 
 | 74 |   return header.magic == 0x73717368 && header.major_version == 4; | 
 | 75 | } | 
 | 76 |  | 
 | 77 | bool GetFileMapContent(const string& sqfs_path, string* map) { | 
 | 78 |   // Create a tmp file | 
 | 79 |   string map_file; | 
 | 80 |   TEST_AND_RETURN_FALSE( | 
 | 81 |       utils::MakeTempFile("squashfs_file_map.XXXXXX", &map_file, nullptr)); | 
 | 82 |   ScopedPathUnlinker map_unlinker(map_file); | 
 | 83 |  | 
 | 84 |   // Run unsquashfs to get the system file map. | 
 | 85 |   // unsquashfs -m <map-file> <squashfs-file> | 
 | 86 |   vector<string> cmd = {"unsquashfs", "-m", map_file, sqfs_path}; | 
 | 87 |   string stdout; | 
 | 88 |   int exit_code; | 
 | 89 |   if (!Subprocess::SynchronousExec(cmd, &exit_code, &stdout) || | 
 | 90 |       exit_code != 0) { | 
 | 91 |     LOG(ERROR) << "Failed to run unsquashfs -m. The stdout content was: " | 
 | 92 |                << stdout; | 
 | 93 |     return false; | 
 | 94 |   } | 
 | 95 |   TEST_AND_RETURN_FALSE(utils::ReadFile(map_file, map)); | 
 | 96 |   return true; | 
 | 97 | } | 
 | 98 |  | 
 | 99 | }  // namespace | 
 | 100 |  | 
 | 101 | bool SquashfsFilesystem::Init(const string& map, | 
| Amin Hassani | 3cd4df1 | 2017-08-25 11:21:53 -0700 | [diff] [blame] | 102 |                               const string& sqfs_path, | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 103 |                               size_t size, | 
| Amin Hassani | 3cd4df1 | 2017-08-25 11:21:53 -0700 | [diff] [blame] | 104 |                               const SquashfsHeader& header, | 
 | 105 |                               bool extract_deflates) { | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 106 |   size_ = size; | 
| Amin Hassani | 3cd4df1 | 2017-08-25 11:21:53 -0700 | [diff] [blame] | 107 |  | 
 | 108 |   bool is_zlib = header.compression_type == kSquashfsZlibCompression; | 
 | 109 |   if (!is_zlib) { | 
 | 110 |     LOG(WARNING) << "Filesystem is not Gzipped. Not filling deflates!"; | 
 | 111 |   } | 
 | 112 |   vector<puffin::ByteExtent> zlib_blks; | 
 | 113 |  | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 114 |   // Reading files map. For the format of the file map look at the comments for | 
 | 115 |   // |CreateFromFileMap()|. | 
 | 116 |   auto lines = base::SplitStringPiece(map, | 
 | 117 |                                       "\n", | 
 | 118 |                                       base::WhitespaceHandling::KEEP_WHITESPACE, | 
 | 119 |                                       base::SplitResult::SPLIT_WANT_NONEMPTY); | 
 | 120 |   for (const auto& line : lines) { | 
 | 121 |     auto splits = | 
 | 122 |         base::SplitStringPiece(line, | 
 | 123 |                                " \t", | 
 | 124 |                                base::WhitespaceHandling::TRIM_WHITESPACE, | 
 | 125 |                                base::SplitResult::SPLIT_WANT_NONEMPTY); | 
 | 126 |     // Only filename is invalid. | 
 | 127 |     TEST_AND_RETURN_FALSE(splits.size() > 1); | 
 | 128 |     uint64_t start; | 
 | 129 |     TEST_AND_RETURN_FALSE(base::StringToUint64(splits[1], &start)); | 
 | 130 |     uint64_t cur_offset = start; | 
 | 131 |     for (size_t i = 2; i < splits.size(); ++i) { | 
 | 132 |       uint64_t blk_size; | 
 | 133 |       TEST_AND_RETURN_FALSE(base::StringToUint64(splits[i], &blk_size)); | 
 | 134 |       // TODO(ahassani): For puffin push it into a proper list if uncompressed. | 
 | 135 |       auto new_blk_size = blk_size & ~kSquashfsCompressedBit; | 
 | 136 |       TEST_AND_RETURN_FALSE(new_blk_size <= header.block_size); | 
| Amin Hassani | 3cd4df1 | 2017-08-25 11:21:53 -0700 | [diff] [blame] | 137 |       if (new_blk_size > 0 && !(blk_size & kSquashfsCompressedBit)) { | 
 | 138 |         // Compressed block | 
 | 139 |         if (is_zlib && extract_deflates) { | 
 | 140 |           zlib_blks.emplace_back(cur_offset, new_blk_size); | 
 | 141 |         } | 
 | 142 |       } | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 143 |       cur_offset += new_blk_size; | 
 | 144 |     } | 
 | 145 |  | 
 | 146 |     // If size is zero do not add the file. | 
 | 147 |     if (cur_offset - start > 0) { | 
 | 148 |       File file; | 
 | 149 |       file.name = splits[0].as_string(); | 
 | 150 |       file.extents = {ExtentForBytes(kBlockSize, start, cur_offset - start)}; | 
 | 151 |       files_.emplace_back(file); | 
 | 152 |     } | 
 | 153 |   } | 
 | 154 |  | 
 | 155 |   // Sort all files by their offset in the squashfs. | 
 | 156 |   std::sort(files_.begin(), files_.end(), [](const File& a, const File& b) { | 
 | 157 |     return a.extents[0].start_block() < b.extents[0].start_block(); | 
 | 158 |   }); | 
 | 159 |   // If there is any overlap between two consecutive extents, remove them. Here | 
 | 160 |   // we are assuming all files have exactly one extent. If this assumption | 
 | 161 |   // changes then this implementation needs to change too. | 
 | 162 |   for (auto first = files_.begin(), second = first + 1; | 
 | 163 |        first != files_.end() && second != files_.end(); | 
 | 164 |        second = first + 1) { | 
 | 165 |     auto first_begin = first->extents[0].start_block(); | 
 | 166 |     auto first_end = first_begin + first->extents[0].num_blocks(); | 
 | 167 |     auto second_begin = second->extents[0].start_block(); | 
 | 168 |     auto second_end = second_begin + second->extents[0].num_blocks(); | 
 | 169 |     // Remove the first file if the size is zero. | 
 | 170 |     if (first_end == first_begin) { | 
 | 171 |       first = files_.erase(first); | 
 | 172 |     } else if (first_end > second_begin) {  // We found a collision. | 
 | 173 |       if (second_end <= first_end) { | 
 | 174 |         // Second file is inside the first file, remove the second file. | 
 | 175 |         second = files_.erase(second); | 
 | 176 |       } else if (first_begin == second_begin) { | 
 | 177 |         // First file is inside the second file, remove the first file. | 
 | 178 |         first = files_.erase(first); | 
 | 179 |       } else { | 
 | 180 |         // Remove overlapping extents from the first file. | 
 | 181 |         first->extents[0].set_num_blocks(second_begin - first_begin); | 
 | 182 |         ++first; | 
 | 183 |       } | 
 | 184 |     } else { | 
 | 185 |       ++first; | 
 | 186 |     } | 
 | 187 |   } | 
 | 188 |  | 
 | 189 |   // Find all the metadata including superblock and add them to the list of | 
 | 190 |   // files. | 
 | 191 |   ExtentRanges file_extents; | 
 | 192 |   for (const auto& file : files_) { | 
 | 193 |     file_extents.AddExtents(file.extents); | 
 | 194 |   } | 
 | 195 |   vector<Extent> full = { | 
 | 196 |       ExtentForRange(0, (size_ + kBlockSize - 1) / kBlockSize)}; | 
 | 197 |   auto metadata_extents = FilterExtentRanges(full, file_extents); | 
 | 198 |   // For now there should be at most two extents. One for superblock and one for | 
 | 199 |   // metadata at the end. Just create appropriate files with <metadata-i> name. | 
 | 200 |   // We can add all these extents as one metadata too, but that violates the | 
 | 201 |   // contiguous write optimization. | 
 | 202 |   for (size_t i = 0; i < metadata_extents.size(); i++) { | 
 | 203 |     File file; | 
 | 204 |     file.name = "<metadata-" + std::to_string(i) + ">"; | 
 | 205 |     file.extents = {metadata_extents[i]}; | 
 | 206 |     files_.emplace_back(file); | 
 | 207 |   } | 
 | 208 |  | 
 | 209 |   // Do one last sort before returning. | 
 | 210 |   std::sort(files_.begin(), files_.end(), [](const File& a, const File& b) { | 
 | 211 |     return a.extents[0].start_block() < b.extents[0].start_block(); | 
 | 212 |   }); | 
| Amin Hassani | 3cd4df1 | 2017-08-25 11:21:53 -0700 | [diff] [blame] | 213 |  | 
 | 214 |   if (is_zlib && extract_deflates) { | 
 | 215 |     // If it is infact gzipped, then the sqfs_path should be valid to read its | 
 | 216 |     // content. | 
 | 217 |     TEST_AND_RETURN_FALSE(!sqfs_path.empty()); | 
 | 218 |     if (zlib_blks.empty()) { | 
 | 219 |       return true; | 
 | 220 |     } | 
 | 221 |  | 
 | 222 |     // Sort zlib blocks. | 
 | 223 |     std::sort(zlib_blks.begin(), | 
 | 224 |               zlib_blks.end(), | 
 | 225 |               [](const puffin::ByteExtent& a, const puffin::ByteExtent& b) { | 
 | 226 |                 return a.offset < b.offset; | 
 | 227 |               }); | 
 | 228 |  | 
 | 229 |     // Sanity check. Make sure zlib blocks are not overlapping. | 
 | 230 |     auto result = std::adjacent_find( | 
 | 231 |         zlib_blks.begin(), | 
 | 232 |         zlib_blks.end(), | 
 | 233 |         [](const puffin::ByteExtent& a, const puffin::ByteExtent& b) { | 
 | 234 |           return (a.offset + a.length) > b.offset; | 
 | 235 |         }); | 
 | 236 |     TEST_AND_RETURN_FALSE(result == zlib_blks.end()); | 
 | 237 |  | 
 | 238 |     vector<puffin::BitExtent> deflates; | 
 | 239 |     TEST_AND_RETURN_FALSE( | 
 | 240 |         puffin::LocateDeflatesInZlibBlocks(sqfs_path, zlib_blks, &deflates)); | 
 | 241 |  | 
 | 242 |     // Add deflates for each file. | 
 | 243 |     for (auto& file : files_) { | 
 | 244 |       file.deflates = deflate_utils::FindDeflates(file.extents, deflates); | 
 | 245 |     } | 
 | 246 |   } | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 247 |   return true; | 
 | 248 | } | 
 | 249 |  | 
 | 250 | unique_ptr<SquashfsFilesystem> SquashfsFilesystem::CreateFromFile( | 
| Amin Hassani | 3cd4df1 | 2017-08-25 11:21:53 -0700 | [diff] [blame] | 251 |     const string& sqfs_path, bool extract_deflates) { | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 252 |   if (sqfs_path.empty()) | 
 | 253 |     return nullptr; | 
 | 254 |  | 
 | 255 |   brillo::StreamPtr sqfs_file = | 
 | 256 |       brillo::FileStream::Open(base::FilePath(sqfs_path), | 
 | 257 |                                brillo::Stream::AccessMode::READ, | 
 | 258 |                                brillo::FileStream::Disposition::OPEN_EXISTING, | 
 | 259 |                                nullptr); | 
 | 260 |   if (!sqfs_file) { | 
 | 261 |     LOG(ERROR) << "Unable to open " << sqfs_path << " for reading."; | 
 | 262 |     return nullptr; | 
 | 263 |   } | 
 | 264 |  | 
 | 265 |   SquashfsHeader header; | 
 | 266 |   brillo::Blob blob(kSquashfsSuperBlockSize); | 
 | 267 |   if (!sqfs_file->ReadAllBlocking(blob.data(), blob.size(), nullptr)) { | 
 | 268 |     LOG(ERROR) << "Unable to read from file: " << sqfs_path; | 
 | 269 |     return nullptr; | 
 | 270 |   } | 
 | 271 |   if (!ReadSquashfsHeader(blob, &header) || !CheckHeader(header)) { | 
 | 272 |     // This is not necessary an error. | 
 | 273 |     return nullptr; | 
 | 274 |   } | 
 | 275 |  | 
 | 276 |   // Read the map file. | 
 | 277 |   string filemap; | 
 | 278 |   if (!GetFileMapContent(sqfs_path, &filemap)) { | 
 | 279 |     LOG(ERROR) << "Failed to produce squashfs map file: " << sqfs_path; | 
 | 280 |     return nullptr; | 
 | 281 |   } | 
 | 282 |  | 
 | 283 |   unique_ptr<SquashfsFilesystem> sqfs(new SquashfsFilesystem()); | 
| Amin Hassani | 3cd4df1 | 2017-08-25 11:21:53 -0700 | [diff] [blame] | 284 |   if (!sqfs->Init( | 
 | 285 |           filemap, sqfs_path, sqfs_file->GetSize(), header, extract_deflates)) { | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 286 |     LOG(ERROR) << "Failed to initialized the Squashfs file system"; | 
 | 287 |     return nullptr; | 
 | 288 |   } | 
| Amin Hassani | 3cd4df1 | 2017-08-25 11:21:53 -0700 | [diff] [blame] | 289 |  | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 290 |   return sqfs; | 
 | 291 | } | 
 | 292 |  | 
 | 293 | unique_ptr<SquashfsFilesystem> SquashfsFilesystem::CreateFromFileMap( | 
 | 294 |     const string& filemap, size_t size, const SquashfsHeader& header) { | 
 | 295 |   if (!CheckHeader(header)) { | 
 | 296 |     LOG(ERROR) << "Invalid Squashfs super block!"; | 
 | 297 |     return nullptr; | 
 | 298 |   } | 
 | 299 |  | 
 | 300 |   unique_ptr<SquashfsFilesystem> sqfs(new SquashfsFilesystem()); | 
| Amin Hassani | 3cd4df1 | 2017-08-25 11:21:53 -0700 | [diff] [blame] | 301 |   if (!sqfs->Init(filemap, "", size, header, false)) { | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 302 |     LOG(ERROR) << "Failed to initialize the Squashfs file system using filemap"; | 
 | 303 |     return nullptr; | 
 | 304 |   } | 
 | 305 |   // TODO(ahassani): Add a function that initializes the puffin related extents. | 
 | 306 |   return sqfs; | 
 | 307 | } | 
 | 308 |  | 
 | 309 | size_t SquashfsFilesystem::GetBlockSize() const { | 
 | 310 |   return kBlockSize; | 
 | 311 | } | 
 | 312 |  | 
 | 313 | size_t SquashfsFilesystem::GetBlockCount() const { | 
 | 314 |   return size_ / kBlockSize; | 
 | 315 | } | 
 | 316 |  | 
 | 317 | bool SquashfsFilesystem::GetFiles(vector<File>* files) const { | 
 | 318 |   files->insert(files->end(), files_.begin(), files_.end()); | 
 | 319 |   return true; | 
 | 320 | } | 
 | 321 |  | 
 | 322 | bool SquashfsFilesystem::LoadSettings(brillo::KeyValueStore* store) const { | 
 | 323 |   // Settings not supported in squashfs. | 
 | 324 |   LOG(ERROR) << "squashfs doesn't support LoadSettings()."; | 
 | 325 |   return false; | 
 | 326 | } | 
 | 327 |  | 
 | 328 | bool SquashfsFilesystem::IsSquashfsImage(const brillo::Blob& blob) { | 
 | 329 |   SquashfsHeader header; | 
 | 330 |   return ReadSquashfsHeader(blob, &header) && CheckHeader(header); | 
 | 331 | } | 
| Amin Hassani | d7da8f4 | 2017-08-23 14:29:40 -0700 | [diff] [blame] | 332 | }  // namespace chromeos_update_engine |