#pragma once #include namespace nall { namespace Beat { struct Archive { static auto create(const string& beatname, const string& pathname, const string& metadata = "") -> bool; static auto unpack(const string& beatname, const string& pathname) -> bool; static auto extract(const string& beatname, const string& pathname) -> vector; private: static auto scan(string_vector& result, const string& basename, const string& pathname) -> void; }; auto Archive::create(const string& beatname, const string& pathname, const string& metadata) -> bool { if(!beatname.endsWith(".bpa") || !pathname.endsWith("/")) return false; //protect against reversed arguments File beat{beatname, file::mode::write}; if(!beat) return false; //file not writable? beat.writes("BPA1"); beat.writevu(metadata.size()); beat.writes(metadata); string_vector contents; scan(contents, pathname, pathname); for(auto& name : contents) { string location{pathname, name}; bool directory = name.endsWith("/"); bool writable = inode::writable(location); bool executable = inode::executable(location); uint info = directory << 0 | writable << 1 | executable << 2 | (name.trimRight("/").size() - 1) << 3; beat.writevu(info); beat.writes(name); if(directory) continue; File input{location, file::mode::read}; if(input) { auto size = input.size(); beat.writevu(size); while(size--) beat.write(input.read()); beat.writel(input.checksum.digest().hex(), 4); } else { beat.writevu(0); beat.writel(0x00000000, 4); } } beat.writel(beat.checksum.digest().hex(), 4); return true; } auto Archive::unpack(const string& beatname, const string& pathname) -> bool { if(!beatname.endsWith(".bpa") || !pathname.endsWith("/")) return false; //protect against reversed arguments File beat{beatname, file::mode::read}; if(!beat) return false; //file not readable? if(beat.reads(4) != "BPA1") return false; auto size = beat.readvu(); while(size--) beat.read(); directory::create(pathname); while(beat.offset() < beat.size() - 4) { auto info = beat.readvu(); auto name = beat.reads((info >> 3) + 1); if(name.find("\\") || name.find("../")) return false; //block path exploits string location{pathname, name}; bool directory = info & 1; bool writable = info & 2; bool executable = info & 4; if(directory) { if(!nall::directory::create(location)) return false; } else { File output{location, file::mode::write}; if(!output) return false; auto size = beat.readvu(); while(size--) output.write(beat.read()); if(beat.readl(4) != output.checksum.digest().hex()) return false; } } uint32_t checksum = beat.checksum.digest().hex(); return beat.readl(4) == checksum; } auto Archive::extract(const string& beatname, const string& filename) -> vector { File beat{beatname, file::mode::read}; if(!beat) return {}; //file not readable? if(beat.reads(4) != "BPA1") return {}; auto size = beat.readvu(); beat.seek(beat.offset() + size); while(beat.offset() < beat.size() - 4) { auto info = beat.readvu(); auto name = beat.reads((info >> 3) + 1); if(info & 1) continue; //ignore directories auto size = beat.readvu(); if(name != filename) { beat.seek(beat.offset() + size + 4); continue; } vector result; result.resize(size); beat.checksum.reset(); for(auto n : range(size)) result[n] = beat.read(); uint32_t checksum = beat.checksum.digest().hex(); if(beat.readl(4) != checksum) return {}; return result; } return {}; } auto Archive::scan(string_vector& result, const string& basename, const string& pathname) -> void { for(auto& name : directory::contents(pathname)) { result.append(string{pathname, name}.trimLeft(basename, 1L)); if(name.endsWith("/")) scan(result, basename, {pathname, name}); } } }}