1 // Copyright 2012 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #ifdef UNSAFE_BUFFERS_BUILD
6 // TODO(crbug.com/40284755): Remove this and spanify to fix the errors.
7 #pragma allow_unsafe_buffers
8 #endif
9
10 // Performs basic inspection of the disk cache files with minimal disruption
11 // to the actual files (they still may change if an error is detected on the
12 // files).
13
14 #include "net/tools/dump_cache/dump_files.h"
15
16 #include <stdio.h>
17
18 #include <memory>
19 #include <set>
20 #include <string>
21
22 #include "base/command_line.h"
23 #include "base/files/file.h"
24 #include "base/files/file_enumerator.h"
25 #include "base/files/file_util.h"
26 #include "base/format_macros.h"
27 #include "base/i18n/time_formatting.h"
28 #include "base/message_loop/message_pump_type.h"
29 #include "base/strings/string_number_conversions.h"
30 #include "base/strings/stringprintf.h"
31 #include "base/task/single_thread_task_executor.h"
32 #include "base/time/time.h"
33 #include "net/disk_cache/blockfile/block_files.h"
34 #include "net/disk_cache/blockfile/disk_format.h"
35 #include "net/disk_cache/blockfile/mapped_file.h"
36 #include "net/disk_cache/blockfile/stats.h"
37 #include "net/disk_cache/blockfile/storage_block-inl.h"
38 #include "net/disk_cache/blockfile/storage_block.h"
39 #include "net/url_request/view_cache_helper.h"
40
41 namespace {
42
43 const base::FilePath::CharType kIndexName[] = FILE_PATH_LITERAL("index");
44
45 // Reads the |header_size| bytes from the beginning of file |name|.
ReadHeader(const base::FilePath & name,char * header,int header_size)46 bool ReadHeader(const base::FilePath& name, char* header, int header_size) {
47 base::File file(name, base::File::FLAG_OPEN | base::File::FLAG_READ);
48 if (!file.IsValid()) {
49 printf("Unable to open file %s\n", name.MaybeAsASCII().c_str());
50 return false;
51 }
52
53 int read = file.Read(0, header, header_size);
54 if (read != header_size) {
55 printf("Unable to read file %s\n", name.MaybeAsASCII().c_str());
56 return false;
57 }
58 return true;
59 }
60
GetMajorVersionFromIndexFile(const base::FilePath & name)61 int GetMajorVersionFromIndexFile(const base::FilePath& name) {
62 disk_cache::IndexHeader header;
63 if (!ReadHeader(name, reinterpret_cast<char*>(&header), sizeof(header)))
64 return 0;
65 if (header.magic != disk_cache::kIndexMagic) {
66 return 0;
67 }
68 return header.version;
69 }
70
GetMajorVersionFromBlockFile(const base::FilePath & name)71 int GetMajorVersionFromBlockFile(const base::FilePath& name) {
72 disk_cache::BlockFileHeader header;
73 if (!ReadHeader(name, reinterpret_cast<char*>(&header), sizeof(header))) {
74 return 0;
75 }
76
77 if (header.magic != disk_cache::kBlockMagic) {
78 return 0;
79 }
80
81 return header.version;
82 }
83
84 // Dumps the contents of the Stats record.
DumpStats(const base::FilePath & path,disk_cache::CacheAddr addr)85 void DumpStats(const base::FilePath& path, disk_cache::CacheAddr addr) {
86 // We need a task executor, although we really don't run any task.
87 base::SingleThreadTaskExecutor io_task_executor(base::MessagePumpType::IO);
88
89 disk_cache::BlockFiles block_files(path);
90 if (!block_files.Init(false)) {
91 printf("Unable to init block files\n");
92 return;
93 }
94
95 disk_cache::Addr address(addr);
96 disk_cache::MappedFile* file = block_files.GetFile(address);
97 if (!file)
98 return;
99
100 size_t length = (2 + disk_cache::Stats::kDataSizesLength) * sizeof(int32_t) +
101 disk_cache::Stats::MAX_COUNTER * sizeof(int64_t);
102
103 size_t offset = address.start_block() * address.BlockSize() +
104 disk_cache::kBlockHeaderSize;
105
106 auto buffer = std::make_unique<int32_t[]>(length);
107 if (!file->Read(buffer.get(), length, offset))
108 return;
109
110 printf("Stats:\nSignatrure: 0x%x\n", buffer[0]);
111 printf("Total size: %d\n", buffer[1]);
112 for (int i = 0; i < disk_cache::Stats::kDataSizesLength; i++)
113 printf("Size(%d): %d\n", i, buffer[i + 2]);
114
115 int64_t* counters = reinterpret_cast<int64_t*>(
116 buffer.get() + 2 + disk_cache::Stats::kDataSizesLength);
117 for (int i = 0; i < disk_cache::Stats::MAX_COUNTER; i++)
118 printf("Count(%d): %" PRId64 "\n", i, *counters++);
119 printf("-------------------------\n\n");
120 }
121
122 // Dumps the contents of the Index-file header.
DumpIndexHeader(const base::FilePath & name,disk_cache::CacheAddr * stats_addr)123 void DumpIndexHeader(const base::FilePath& name,
124 disk_cache::CacheAddr* stats_addr) {
125 disk_cache::IndexHeader header;
126 if (!ReadHeader(name, reinterpret_cast<char*>(&header), sizeof(header)))
127 return;
128
129 printf("Index file:\n");
130 printf("magic: %x\n", header.magic);
131 printf("version: %d.%d\n", header.version >> 16, header.version & 0xffff);
132 printf("entries: %d\n", header.num_entries);
133 printf("total bytes: %" PRId64 "\n", header.num_bytes);
134 printf("last file number: %d\n", header.last_file);
135 printf("current id: %d\n", header.this_id);
136 printf("table length: %d\n", header.table_len);
137 printf("last crash: %d\n", header.crash);
138 printf("experiment: %d\n", header.experiment);
139 printf("stats: %x\n", header.stats);
140 for (int i = 0; i < 5; i++) {
141 printf("head %d: 0x%x\n", i, header.lru.heads[i]);
142 printf("tail %d: 0x%x\n", i, header.lru.tails[i]);
143 printf("size %d: 0x%x\n", i, header.lru.sizes[i]);
144 }
145 printf("transaction: 0x%x\n", header.lru.transaction);
146 printf("operation: %d\n", header.lru.operation);
147 printf("operation list: %d\n", header.lru.operation_list);
148 printf("-------------------------\n\n");
149
150 if (stats_addr)
151 *stats_addr = header.stats;
152 }
153
154 // Dumps the contents of a block-file header.
DumpBlockHeader(const base::FilePath & name)155 void DumpBlockHeader(const base::FilePath& name) {
156 disk_cache::BlockFileHeader header;
157 if (!ReadHeader(name, reinterpret_cast<char*>(&header), sizeof(header)))
158 return;
159
160 printf("Block file: %s\n", name.BaseName().MaybeAsASCII().c_str());
161 printf("magic: %x\n", header.magic);
162 printf("version: %d.%d\n", header.version >> 16, header.version & 0xffff);
163 printf("file id: %d\n", header.this_file);
164 printf("next file id: %d\n", header.next_file);
165 printf("entry size: %d\n", header.entry_size);
166 printf("current entries: %d\n", header.num_entries);
167 printf("max entries: %d\n", header.max_entries);
168 printf("updating: %d\n", header.updating);
169 printf("empty sz 1: %d\n", header.empty[0]);
170 printf("empty sz 2: %d\n", header.empty[1]);
171 printf("empty sz 3: %d\n", header.empty[2]);
172 printf("empty sz 4: %d\n", header.empty[3]);
173 printf("user 0: 0x%x\n", header.user[0]);
174 printf("user 1: 0x%x\n", header.user[1]);
175 printf("user 2: 0x%x\n", header.user[2]);
176 printf("user 3: 0x%x\n", header.user[3]);
177 printf("-------------------------\n\n");
178 }
179
180 // Simple class that interacts with the set of cache files.
181 class CacheDumper {
182 public:
CacheDumper(const base::FilePath & path)183 explicit CacheDumper(const base::FilePath& path)
184 : path_(path), block_files_(path) {}
185
186 CacheDumper(const CacheDumper&) = delete;
187 CacheDumper& operator=(const CacheDumper&) = delete;
188
189 bool Init();
190
191 // Reads an entry from disk. Return false when all entries have been already
192 // returned.
193 bool GetEntry(disk_cache::EntryStore* entry, disk_cache::CacheAddr* addr);
194
195 // Loads a specific block from the block files.
196 bool LoadEntry(disk_cache::CacheAddr addr, disk_cache::EntryStore* entry);
197 bool LoadRankings(disk_cache::CacheAddr addr,
198 disk_cache::RankingsNode* rankings);
199
200 // Appends the data store at |addr| to |out|.
201 bool HexDump(disk_cache::CacheAddr addr, std::string* out);
202
203 private:
204 base::FilePath path_;
205 disk_cache::BlockFiles block_files_;
206 scoped_refptr<disk_cache::MappedFile> index_file_;
207 disk_cache::Index* index_ = nullptr;
208 int current_hash_ = 0;
209 disk_cache::CacheAddr next_addr_ = 0;
210 std::set<disk_cache::CacheAddr> dumped_entries_;
211 };
212
Init()213 bool CacheDumper::Init() {
214 if (!block_files_.Init(false)) {
215 printf("Unable to init block files\n");
216 return false;
217 }
218
219 base::FilePath index_name(path_.Append(kIndexName));
220 index_file_ = base::MakeRefCounted<disk_cache::MappedFile>();
221 index_ = reinterpret_cast<disk_cache::Index*>(
222 index_file_->Init(index_name, 0));
223 if (!index_) {
224 printf("Unable to map index\n");
225 return false;
226 }
227
228 return true;
229 }
230
GetEntry(disk_cache::EntryStore * entry,disk_cache::CacheAddr * addr)231 bool CacheDumper::GetEntry(disk_cache::EntryStore* entry,
232 disk_cache::CacheAddr* addr) {
233 if (dumped_entries_.find(next_addr_) != dumped_entries_.end()) {
234 printf("Loop detected\n");
235 next_addr_ = 0;
236 current_hash_++;
237 }
238
239 if (next_addr_) {
240 *addr = next_addr_;
241 if (LoadEntry(next_addr_, entry))
242 return true;
243
244 printf("Unable to load entry at address 0x%x\n", next_addr_);
245 next_addr_ = 0;
246 current_hash_++;
247 }
248
249 for (int i = current_hash_; i < index_->header.table_len; i++) {
250 // Yes, we'll crash if the table is shorter than expected, but only after
251 // dumping every entry that we can find.
252 if (index_->table[i]) {
253 current_hash_ = i;
254 *addr = index_->table[i];
255 if (LoadEntry(index_->table[i], entry))
256 return true;
257
258 printf("Unable to load entry at address 0x%x\n", index_->table[i]);
259 }
260 }
261 return false;
262 }
263
LoadEntry(disk_cache::CacheAddr addr,disk_cache::EntryStore * entry)264 bool CacheDumper::LoadEntry(disk_cache::CacheAddr addr,
265 disk_cache::EntryStore* entry) {
266 disk_cache::Addr address(addr);
267 disk_cache::MappedFile* file = block_files_.GetFile(address);
268 if (!file)
269 return false;
270
271 disk_cache::StorageBlock<disk_cache::EntryStore> entry_block(file, address);
272 if (!entry_block.Load())
273 return false;
274
275 memcpy(entry, entry_block.Data(), sizeof(*entry));
276 if (!entry_block.VerifyHash())
277 printf("Self hash failed at 0x%x\n", addr);
278
279 // Prepare for the next entry to load.
280 next_addr_ = entry->next;
281 if (next_addr_) {
282 dumped_entries_.insert(addr);
283 } else {
284 current_hash_++;
285 dumped_entries_.clear();
286 }
287 return true;
288 }
289
LoadRankings(disk_cache::CacheAddr addr,disk_cache::RankingsNode * rankings)290 bool CacheDumper::LoadRankings(disk_cache::CacheAddr addr,
291 disk_cache::RankingsNode* rankings) {
292 disk_cache::Addr address(addr);
293 if (address.file_type() != disk_cache::RANKINGS)
294 return false;
295
296 disk_cache::MappedFile* file = block_files_.GetFile(address);
297 if (!file)
298 return false;
299
300 disk_cache::StorageBlock<disk_cache::RankingsNode> rank_block(file, address);
301 if (!rank_block.Load())
302 return false;
303
304 if (!rank_block.VerifyHash())
305 printf("Self hash failed at 0x%x\n", addr);
306
307 memcpy(rankings, rank_block.Data(), sizeof(*rankings));
308 return true;
309 }
310
HexDump(disk_cache::CacheAddr addr,std::string * out)311 bool CacheDumper::HexDump(disk_cache::CacheAddr addr, std::string* out) {
312 disk_cache::Addr address(addr);
313 disk_cache::MappedFile* file = block_files_.GetFile(address);
314 if (!file)
315 return false;
316
317 size_t size = address.num_blocks() * address.BlockSize();
318 auto buffer = std::make_unique<char[]>(size);
319
320 size_t offset = address.start_block() * address.BlockSize() +
321 disk_cache::kBlockHeaderSize;
322 if (!file->Read(buffer.get(), size, offset))
323 return false;
324
325 base::StringAppendF(out, "0x%x:\n", addr);
326 net::ViewCacheHelper::HexDump(buffer.get(), size, out);
327 return true;
328 }
329
ToLocalTime(int64_t time_us)330 std::string ToLocalTime(int64_t time_us) {
331 return base::UnlocalizedTimeFormatWithPattern(
332 base::Time::FromDeltaSinceWindowsEpoch(base::Microseconds(time_us)),
333 "y/M/d H:m:s.S");
334 }
335
DumpEntry(disk_cache::CacheAddr addr,const disk_cache::EntryStore & entry,bool verbose)336 void DumpEntry(disk_cache::CacheAddr addr,
337 const disk_cache::EntryStore& entry,
338 bool verbose) {
339 std::string key;
340 static bool full_key =
341 base::CommandLine::ForCurrentProcess()->HasSwitch("full-key");
342 if (!entry.long_key) {
343 key = std::string(entry.key, std::min(static_cast<size_t>(entry.key_len),
344 sizeof(entry.key)));
345 if (entry.key_len > 90 && !full_key)
346 key.resize(90);
347 }
348
349 printf("Entry at 0x%x\n", addr);
350 printf("rankings: 0x%x\n", entry.rankings_node);
351 printf("key length: %d\n", entry.key_len);
352 printf("key: \"%s\"\n", key.c_str());
353
354 if (verbose) {
355 printf("key addr: 0x%x\n", entry.long_key);
356 printf("hash: 0x%x\n", entry.hash);
357 printf("next entry: 0x%x\n", entry.next);
358 printf("reuse count: %d\n", entry.reuse_count);
359 printf("refetch count: %d\n", entry.refetch_count);
360 printf("state: %d\n", entry.state);
361 printf("creation: %s\n", ToLocalTime(entry.creation_time).c_str());
362 for (int i = 0; i < 4; i++) {
363 printf("data size %d: %d\n", i, entry.data_size[i]);
364 printf("data addr %d: 0x%x\n", i, entry.data_addr[i]);
365 }
366 printf("----------\n\n");
367 }
368 }
369
DumpRankings(disk_cache::CacheAddr addr,const disk_cache::RankingsNode & rankings,bool verbose)370 void DumpRankings(disk_cache::CacheAddr addr,
371 const disk_cache::RankingsNode& rankings,
372 bool verbose) {
373 printf("Rankings at 0x%x\n", addr);
374 printf("next: 0x%x\n", rankings.next);
375 printf("prev: 0x%x\n", rankings.prev);
376 printf("entry: 0x%x\n", rankings.contents);
377
378 if (verbose) {
379 printf("dirty: %d\n", rankings.dirty);
380 if (rankings.last_used != rankings.last_modified)
381 printf("used: %s\n", ToLocalTime(rankings.last_used).c_str());
382 printf("modified: %s\n", ToLocalTime(rankings.last_modified).c_str());
383 printf("hash: 0x%x\n", rankings.self_hash);
384 printf("----------\n\n");
385 } else {
386 printf("\n");
387 }
388 }
389
PrintCSVHeader()390 void PrintCSVHeader() {
391 printf(
392 "entry,rankings,next,prev,rank-contents,chain,reuse,key,"
393 "d0,d1,d2,d3\n");
394 }
395
DumpCSV(disk_cache::CacheAddr addr,const disk_cache::EntryStore & entry,const disk_cache::RankingsNode & rankings)396 void DumpCSV(disk_cache::CacheAddr addr,
397 const disk_cache::EntryStore& entry,
398 const disk_cache::RankingsNode& rankings) {
399 printf("0x%x,0x%x,0x%x,0x%x,0x%x,0x%x,0x%x,0x%x,0x%x,0x%x,0x%x,0x%x\n", addr,
400 entry.rankings_node, rankings.next, rankings.prev, rankings.contents,
401 entry.next, entry.reuse_count, entry.long_key, entry.data_addr[0],
402 entry.data_addr[1], entry.data_addr[2], entry.data_addr[3]);
403
404 if (addr != rankings.contents)
405 printf("Broken entry\n");
406 }
407
CanDump(disk_cache::CacheAddr addr)408 bool CanDump(disk_cache::CacheAddr addr) {
409 disk_cache::Addr address(addr);
410 return address.is_initialized() && address.is_block_file();
411 }
412
413 } // namespace.
414
415 // -----------------------------------------------------------------------
416
CheckFileVersion(const base::FilePath & input_path)417 bool CheckFileVersion(const base::FilePath& input_path) {
418 base::FilePath index_name(input_path.Append(kIndexName));
419
420 int index_version = GetMajorVersionFromIndexFile(index_name);
421 if (!index_version || index_version != disk_cache::kVersion3_0) {
422 return false;
423 }
424
425 constexpr int kCurrentBlockVersion = disk_cache::kBlockVersion2;
426 for (int i = 0; i < disk_cache::kFirstAdditionalBlockFile; i++) {
427 std::string data_name = "data_" + base::NumberToString(i);
428 auto data_path = input_path.AppendASCII(data_name);
429 int block_version = GetMajorVersionFromBlockFile(data_path);
430 if (!block_version || block_version != kCurrentBlockVersion) {
431 return false;
432 }
433 }
434 return true;
435 }
436
437 // Dumps the headers of all files.
DumpHeaders(const base::FilePath & input_path)438 int DumpHeaders(const base::FilePath& input_path) {
439 base::FilePath index_name(input_path.Append(kIndexName));
440 disk_cache::CacheAddr stats_addr = 0;
441 DumpIndexHeader(index_name, &stats_addr);
442
443 base::FileEnumerator iter(input_path, false,
444 base::FileEnumerator::FILES,
445 FILE_PATH_LITERAL("data_*"));
446 for (base::FilePath file = iter.Next(); !file.empty(); file = iter.Next())
447 DumpBlockHeader(file);
448
449 DumpStats(input_path, stats_addr);
450 return 0;
451 }
452
453 // Dumps all entries from the cache.
DumpContents(const base::FilePath & input_path)454 int DumpContents(const base::FilePath& input_path) {
455 bool print_csv = base::CommandLine::ForCurrentProcess()->HasSwitch("csv");
456 if (!print_csv)
457 DumpIndexHeader(input_path.Append(kIndexName), nullptr);
458
459 // We need a task executor, although we really don't run any task.
460 base::SingleThreadTaskExecutor io_task_executor(base::MessagePumpType::IO);
461 CacheDumper dumper(input_path);
462 if (!dumper.Init())
463 return -1;
464
465 if (print_csv)
466 PrintCSVHeader();
467
468 disk_cache::EntryStore entry;
469 disk_cache::CacheAddr addr;
470 bool verbose = base::CommandLine::ForCurrentProcess()->HasSwitch("v");
471 while (dumper.GetEntry(&entry, &addr)) {
472 if (!print_csv)
473 DumpEntry(addr, entry, verbose);
474 disk_cache::RankingsNode rankings;
475 if (!dumper.LoadRankings(entry.rankings_node, &rankings))
476 continue;
477
478 if (print_csv)
479 DumpCSV(addr, entry, rankings);
480 else
481 DumpRankings(entry.rankings_node, rankings, verbose);
482 }
483
484 printf("Done.\n");
485
486 return 0;
487 }
488
DumpLists(const base::FilePath & input_path)489 int DumpLists(const base::FilePath& input_path) {
490 base::FilePath index_name(input_path.Append(kIndexName));
491 disk_cache::IndexHeader header;
492 if (!ReadHeader(index_name, reinterpret_cast<char*>(&header), sizeof(header)))
493 return -1;
494
495 // We need a task executor, although we really don't run any task.
496 base::SingleThreadTaskExecutor io_task_executor(base::MessagePumpType::IO);
497 CacheDumper dumper(input_path);
498 if (!dumper.Init())
499 return -1;
500
501 printf("list, addr, next, prev, entry\n");
502
503 const int kMaxLength = 1 * 1000 * 1000;
504 for (int i = 0; i < 5; i++) {
505 int32_t size = header.lru.sizes[i];
506 if (size < 0 || size > kMaxLength) {
507 printf("Wrong size %d\n", size);
508 }
509
510 disk_cache::CacheAddr addr = header.lru.tails[i];
511 int count = 0;
512 while (addr) {
513 count++;
514 disk_cache::RankingsNode rankings;
515 if (!dumper.LoadRankings(addr, &rankings)) {
516 printf("Failed to load node at 0x%x\n", addr);
517 break;
518 }
519 printf("%d, 0x%x, 0x%x, 0x%x, 0x%x\n", i, addr, rankings.next,
520 rankings.prev, rankings.contents);
521
522 if (rankings.prev == addr)
523 break;
524
525 addr = rankings.prev;
526 }
527 printf("%d nodes found, %d reported\n", count, header.lru.sizes[i]);
528 }
529
530 printf("Done.\n");
531 return 0;
532 }
533
DumpEntryAt(const base::FilePath & input_path,const std::string & at)534 int DumpEntryAt(const base::FilePath& input_path, const std::string& at) {
535 disk_cache::CacheAddr addr;
536 if (!base::HexStringToUInt(at, &addr))
537 return -1;
538
539 if (!CanDump(addr))
540 return -1;
541
542 base::FilePath index_name(input_path.Append(kIndexName));
543 disk_cache::IndexHeader header;
544 if (!ReadHeader(index_name, reinterpret_cast<char*>(&header), sizeof(header)))
545 return -1;
546
547 // We need a task executor, although we really don't run any task.
548 base::SingleThreadTaskExecutor io_task_executor(base::MessagePumpType::IO);
549 CacheDumper dumper(input_path);
550 if (!dumper.Init())
551 return -1;
552
553 disk_cache::CacheAddr entry_addr = 0;
554 disk_cache::CacheAddr rankings_addr = 0;
555 disk_cache::Addr address(addr);
556
557 disk_cache::RankingsNode rankings;
558 if (address.file_type() == disk_cache::RANKINGS) {
559 if (dumper.LoadRankings(addr, &rankings)) {
560 rankings_addr = addr;
561 addr = rankings.contents;
562 address = disk_cache::Addr(addr);
563 }
564 }
565
566 disk_cache::EntryStore entry = {};
567 if (address.file_type() == disk_cache::BLOCK_256 &&
568 dumper.LoadEntry(addr, &entry)) {
569 entry_addr = addr;
570 DumpEntry(addr, entry, true);
571 if (!rankings_addr && dumper.LoadRankings(entry.rankings_node, &rankings))
572 rankings_addr = entry.rankings_node;
573 }
574
575 bool verbose = base::CommandLine::ForCurrentProcess()->HasSwitch("v");
576
577 std::string hex_dump;
578 if (!rankings_addr || verbose)
579 dumper.HexDump(addr, &hex_dump);
580
581 if (rankings_addr)
582 DumpRankings(rankings_addr, rankings, true);
583
584 if (entry_addr && verbose) {
585 if (entry.long_key && CanDump(entry.long_key))
586 dumper.HexDump(entry.long_key, &hex_dump);
587
588 for (disk_cache::CacheAddr data_addr : entry.data_addr) {
589 if (data_addr && CanDump(data_addr))
590 dumper.HexDump(data_addr, &hex_dump);
591 }
592 }
593
594 printf("%s\n", hex_dump.c_str());
595 printf("Done.\n");
596 return 0;
597 }
598
DumpAllocation(const base::FilePath & file)599 int DumpAllocation(const base::FilePath& file) {
600 disk_cache::BlockFileHeader header;
601 if (!ReadHeader(file, reinterpret_cast<char*>(&header), sizeof(header)))
602 return -1;
603
604 std::string out;
605 net::ViewCacheHelper::HexDump(reinterpret_cast<char*>(&header.allocation_map),
606 sizeof(header.allocation_map), &out);
607 printf("%s\n", out.c_str());
608 return 0;
609 }
610