// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/sessions/model/session_internal_util.h"
#import "base/apple/foundation_util.h"
#import "base/files/file_path.h"
#import "base/logging.h"
#import "base/strings/sys_string_conversions.h"
#import "ios/chrome/browser/sessions/model/session_ios.h"
#import "ios/chrome/browser/sessions/model/session_window_ios.h"
#import "third_party/protobuf/src/google/protobuf/message_lite.h"
namespace ios::sessions {
namespace internal {
// Indicate the status for a path.
enum class PathStatus {
kFile,
kDirectory,
kInexistent,
};
// Checks the status of `path`.
[[nodiscard]] PathStatus GetPathStatus(NSString* path) {
BOOL is_directory = NO;
if (![[NSFileManager defaultManager] fileExistsAtPath:path
isDirectory:&is_directory]) {
return PathStatus::kInexistent;
}
return is_directory ? PathStatus::kDirectory : PathStatus::kFile;
}
// Returns whether a file named `filename` exists.
[[nodiscard]] bool FileExists(NSString* filename) {
return GetPathStatus(filename) == PathStatus::kFile;
}
// Returns whether a directory named `dirname` exists.
[[nodiscard]] bool DirectoryExists(NSString* dirname) {
return GetPathStatus(dirname) == PathStatus::kDirectory;
}
// Creates `directory` including all intermediate directories and returns
// whether the operation was a success. Safe to call if `directory` exists.
[[nodiscard]] bool CreateDirectory(NSString* directory) {
NSError* error = nil;
if (![[NSFileManager defaultManager] createDirectoryAtPath:directory
withIntermediateDirectories:YES
attributes:nil
error:&error]) {
DLOG(WARNING) << "Error creating directory: "
<< base::SysNSStringToUTF8(directory) << ": "
<< base::SysNSStringToUTF8([error description]);
return false;
}
return true;
}
// Renames a file from `from` to `dest`.
[[nodiscard]] bool RenameFile(NSString* from, NSString* dest) {
if (!CreateDirectory([dest stringByDeletingLastPathComponent])) {
return false;
}
NSError* error = nil;
if (![[NSFileManager defaultManager] moveItemAtPath:from
toPath:dest
error:&error]) {
DLOG(WARNING) << "Error moving file from: " << base::SysNSStringToUTF8(from)
<< " to: " << base::SysNSStringToUTF8(dest) << ": "
<< base::SysNSStringToUTF8([error description]);
return false;
}
return true;
}
// Returns whether `directory` exists and is empty.
[[nodiscard]] bool DirectoryEmpty(NSString* directory) {
if (!DirectoryExists(directory)) {
return false;
}
NSDirectoryEnumerator<NSString*>* enumerator =
[[NSFileManager defaultManager] enumeratorAtPath:directory];
return [enumerator nextObject] == nil;
}
// Deletes recursively file or directory at `path`. Returns whether the
// operation was a success.
[[nodiscard]] bool DeleteRecursively(NSString* path) {
NSError* error = nil;
if (![[NSFileManager defaultManager] removeItemAtPath:path error:&error]) {
DLOG(WARNING) << "Error removing file/directory at: "
<< base::SysNSStringToUTF8(path) << ": "
<< base::SysNSStringToUTF8([error description]);
return false;
}
return true;
}
// Copies content of `from_dir` to `dest_dir` recursively. It is an error
// if `from_dir` is not an existing directory or if `dest_dir` exists and
// is not a directory.
[[nodiscard]] bool CopyDirectory(NSString* from_dir, NSString* dest_dir) {
if (!DirectoryExists(from_dir)) {
DLOG(WARNING) << "Error copying directory: "
<< base::SysNSStringToUTF8(from_dir) << " to "
<< base::SysNSStringToUTF8(dest_dir) << ": no such directory";
return false;
}
switch (GetPathStatus(dest_dir)) {
case PathStatus::kDirectory:
if (!DeleteRecursively(dest_dir)) {
return false;
}
break;
case PathStatus::kFile:
DLOG(WARNING) << "Error copying directory: "
<< base::SysNSStringToUTF8(from_dir) << " to "
<< base::SysNSStringToUTF8(dest_dir) << ": file exists";
return false;
case PathStatus::kInexistent:
break;
}
// Create parent directory of `dest_dir`.
if (!CreateDirectory([dest_dir stringByDeletingLastPathComponent])) {
return false;
}
// Use hardlink to perform the copy to reduce the impact on storage. The
// documentation of -linkItemAtPath:toPath:error: explicitly explain that
// if source is a directory, the method create the destination directory
// and hard-link the content recursively.
NSError* error = nil;
if (![[NSFileManager defaultManager] linkItemAtPath:from_dir
toPath:dest_dir
error:&error]) {
DLOG(WARNING) << "Error copying directory: "
<< base::SysNSStringToUTF8(from_dir) << " to "
<< base::SysNSStringToUTF8(dest_dir) << ": "
<< base::SysNSStringToUTF8([error description]);
return false;
}
return true;
}
// Copies file at `from_path` to `dest_path`. It is an error if `from_path`
// is not a file or if `dest_path` exists and is not a file.
[[nodiscard]] bool CopyFile(NSString* from_path, NSString* dest_path) {
if (!FileExists(from_path)) {
DLOG(WARNING) << "Error copying file: "
<< base::SysNSStringToUTF8(from_path) << " to "
<< base::SysNSStringToUTF8(dest_path) << ": no such file";
return false;
}
switch (GetPathStatus(dest_path)) {
case PathStatus::kDirectory:
DLOG(WARNING) << "Error copying file: "
<< base::SysNSStringToUTF8(from_path) << " to "
<< base::SysNSStringToUTF8(dest_path)
<< ": directory exists";
break;
case PathStatus::kFile:
if (!DeleteRecursively(dest_path)) {
return false;
}
break;
case PathStatus::kInexistent:
break;
}
// Create parent directory of `dest_path`.
if (!CreateDirectory([dest_path stringByDeletingLastPathComponent])) {
return false;
}
// Use hardlink to perform the copy to reduce the impact on storage.
NSError* error = nil;
if (![[NSFileManager defaultManager] linkItemAtPath:from_path
toPath:dest_path
error:&error]) {
DLOG(WARNING) << "Error copying file: "
<< base::SysNSStringToUTF8(from_path) << " to "
<< base::SysNSStringToUTF8(dest_path) << ": "
<< base::SysNSStringToUTF8([error description]);
return false;
}
return true;
}
// Writes `data` to `filename` and returns whether the operation was a success.
// The file is created with protection until first user authentication.
[[nodiscard]] bool WriteFile(NSString* filename, NSData* data) {
if (!CreateDirectory([filename stringByDeletingLastPathComponent])) {
return false;
}
// Options for writing data.
constexpr NSDataWritingOptions options =
NSDataWritingAtomic |
NSDataWritingFileProtectionCompleteUntilFirstUserAuthentication;
NSError* error = nil;
if (![data writeToFile:filename options:options error:&error]) {
DLOG(WARNING) << "Error writing to file: "
<< base::SysNSStringToUTF8(filename) << ": "
<< base::SysNSStringToUTF8([error description]);
return false;
}
return true;
}
// Reads content of `filename` and returns it as a `NSData*` or nil on error.
[[nodiscard]] NSData* ReadFile(NSString* filename) {
NSError* error = nil;
NSData* data = [NSData dataWithContentsOfFile:filename
options:0
error:&error];
if (!data) {
DLOG(WARNING) << "Error loading from file: "
<< base::SysNSStringToUTF8(filename) << ": "
<< base::SysNSStringToUTF8([error description]);
return nil;
}
return data;
}
} // namespace internal
bool FileExists(const base::FilePath& filename) {
return internal::FileExists(base::apple::FilePathToNSString(filename));
}
bool DirectoryExists(const base::FilePath& dirname) {
return internal::DirectoryExists(base::apple::FilePathToNSString(dirname));
}
bool RenameFile(const base::FilePath& from, const base::FilePath& dest) {
return internal::RenameFile(base::apple::FilePathToNSString(from),
base::apple::FilePathToNSString(dest));
}
bool CreateDirectory(const base::FilePath& directory) {
return internal::CreateDirectory(base::apple::FilePathToNSString(directory));
}
bool DirectoryEmpty(const base::FilePath& directory) {
return internal::DirectoryEmpty(base::apple::FilePathToNSString(directory));
}
bool DeleteRecursively(const base::FilePath& path) {
return internal::DeleteRecursively(base::apple::FilePathToNSString(path));
}
bool CopyDirectory(const base::FilePath& from_dir,
const base::FilePath& dest_dir) {
return internal::CopyDirectory(base::apple::FilePathToNSString(from_dir),
base::apple::FilePathToNSString(dest_dir));
}
bool CopyFile(const base::FilePath& from_path,
const base::FilePath& dest_path) {
return internal::CopyFile(base::apple::FilePathToNSString(from_path),
base::apple::FilePathToNSString(dest_path));
}
bool WriteFile(const base::FilePath& filename, NSData* data) {
return internal::WriteFile(base::apple::FilePathToNSString(filename), data);
}
NSData* ReadFile(const base::FilePath& filename) {
return internal::ReadFile(base::apple::FilePathToNSString(filename));
}
bool WriteProto(const base::FilePath& filename,
const google::protobuf::MessageLite& proto) {
@autoreleasepool {
// Allocate a NSData object large enough to hold the serialized protobuf.
const size_t serialized_size = proto.ByteSizeLong();
NSMutableData* data = [NSMutableData dataWithLength:serialized_size];
if (!proto.SerializeToArray(data.mutableBytes, data.length)) {
DLOG(WARNING) << "Error serializing proto to file: "
<< filename.AsUTF8Unsafe();
return false;
}
return WriteFile(filename, data);
}
}
bool ParseProto(const base::FilePath& filename,
google::protobuf::MessageLite& proto) {
NSData* data = ReadFile(filename);
if (!data) {
return false;
}
if (!proto.ParseFromArray(data.bytes, data.length)) {
DLOG(WARNING) << "Error parsing proto from file: "
<< filename.AsUTF8Unsafe();
return false;
}
return true;
}
NSData* ArchiveRootObject(NSObject<NSCoding>* object) {
NSError* error = nil;
NSData* archived = [NSKeyedArchiver archivedDataWithRootObject:object
requiringSecureCoding:NO
error:&error];
if (error) {
DLOG(WARNING) << "Error serializing data: "
<< base::SysNSStringToUTF8([error description]);
return nil;
}
return archived;
}
NSObject<NSCoding>* DecodeRootObject(NSData* data) {
NSError* error = nil;
NSKeyedUnarchiver* unarchiver =
[[NSKeyedUnarchiver alloc] initForReadingFromData:data error:&error];
if (error) {
DLOG(WARNING) << "Error deserializing data: "
<< base::SysNSStringToUTF8([error description]);
return nil;
}
unarchiver.requiresSecureCoding = NO;
// -decodeObjectForKey: propagates exception, so wrap the call in
// @try/@catch block to prevent them from terminating the app.
@try {
return [unarchiver decodeObjectForKey:@"root"];
} @catch (NSException* exception) {
DLOG(WARNING) << "Error deserializing data: "
<< base::SysNSStringToUTF8([exception description]);
return nil;
}
}
SessionWindowIOS* ReadSessionWindow(const base::FilePath& filename) {
NSData* data = ReadFile(filename);
if (!data) {
return nil;
}
NSObject* root = DecodeRootObject(data);
if (!root) {
return nil;
}
if ([root isKindOfClass:[SessionIOS class]]) {
SessionIOS* session = base::apple::ObjCCastStrict<SessionIOS>(root);
if (session.sessionWindows.count != 1) {
DLOG(WARNING) << "Error deserializing data: "
<< "not exactly one SessionWindowIOS.";
return nil;
}
return session.sessionWindows[0];
}
return base::apple::ObjCCast<SessionWindowIOS>(root);
}
bool WriteSessionWindow(const base::FilePath& filename,
SessionWindowIOS* session) {
NSData* data = ArchiveRootObject(session);
if (!data) {
return false;
}
return WriteFile(filename, data);
}
} // namespace ios::sessions