Serenity Operating System
at master 249 lines 8.0 kB view raw
1/* 2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> 3 * Copyright (c) 2021, networkException <networkexception@serenityos.org> 4 * Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org> 5 * 6 * SPDX-License-Identifier: BSD-2-Clause 7 */ 8 9#include <AK/LexicalPath.h> 10#include <AK/StringBuilder.h> 11#include <LibCore/ConfigFile.h> 12#include <LibCore/Directory.h> 13#include <LibCore/StandardPaths.h> 14#include <LibCore/System.h> 15#include <pwd.h> 16#include <sys/types.h> 17 18namespace Core { 19 20ErrorOr<NonnullRefPtr<ConfigFile>> ConfigFile::open_for_lib(DeprecatedString const& lib_name, AllowWriting allow_altering) 21{ 22 DeprecatedString directory_name = DeprecatedString::formatted("{}/lib", StandardPaths::config_directory()); 23 auto directory = TRY(Directory::create(directory_name, Directory::CreateDirectories::Yes)); 24 auto path = DeprecatedString::formatted("{}/{}.ini", directory, lib_name); 25 return ConfigFile::open(path, allow_altering); 26} 27 28ErrorOr<NonnullRefPtr<ConfigFile>> ConfigFile::open_for_app(DeprecatedString const& app_name, AllowWriting allow_altering) 29{ 30 auto directory = TRY(Directory::create(StandardPaths::config_directory(), Directory::CreateDirectories::Yes)); 31 auto path = DeprecatedString::formatted("{}/{}.ini", directory, app_name); 32 return ConfigFile::open(path, allow_altering); 33} 34 35ErrorOr<NonnullRefPtr<ConfigFile>> ConfigFile::open_for_system(DeprecatedString const& app_name, AllowWriting allow_altering) 36{ 37 auto path = DeprecatedString::formatted("/etc/{}.ini", app_name); 38 return ConfigFile::open(path, allow_altering); 39} 40 41ErrorOr<NonnullRefPtr<ConfigFile>> ConfigFile::open(DeprecatedString const& filename, AllowWriting allow_altering) 42{ 43 auto maybe_file = File::open(filename, allow_altering == AllowWriting::Yes ? File::OpenMode::ReadWrite : File::OpenMode::Read); 44 OwnPtr<BufferedFile> buffered_file; 45 if (maybe_file.is_error()) { 46 // If we attempted to open a read-only file that does not exist, we ignore the error, making it appear 47 // the same as if we had opened an empty file. This behavior is a little weird, but is required by 48 // user code, which does not check the config file exists before opening. 49 if (!(allow_altering == AllowWriting::No && maybe_file.error().code() == ENOENT)) 50 return maybe_file.release_error(); 51 } else { 52 buffered_file = TRY(BufferedFile::create(maybe_file.release_value())); 53 } 54 55 auto config_file = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) ConfigFile(filename, move(buffered_file)))); 56 TRY(config_file->reparse()); 57 return config_file; 58} 59 60ErrorOr<NonnullRefPtr<ConfigFile>> ConfigFile::open(DeprecatedString const& filename, int fd) 61{ 62 auto file = TRY(File::adopt_fd(fd, File::OpenMode::ReadWrite)); 63 return open(filename, move(file)); 64} 65 66ErrorOr<NonnullRefPtr<ConfigFile>> ConfigFile::open(DeprecatedString const& filename, NonnullOwnPtr<Core::File> file) 67{ 68 auto buffered_file = TRY(BufferedFile::create(move(file))); 69 70 auto config_file = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) ConfigFile(filename, move(buffered_file)))); 71 TRY(config_file->reparse()); 72 return config_file; 73} 74 75ConfigFile::ConfigFile(DeprecatedString const& filename, OwnPtr<BufferedFile> open_file) 76 : m_filename(filename) 77 , m_file(move(open_file)) 78{ 79} 80 81ConfigFile::~ConfigFile() 82{ 83 MUST(sync()); 84} 85 86ErrorOr<void> ConfigFile::reparse() 87{ 88 m_groups.clear(); 89 if (!m_file) 90 return {}; 91 92 HashMap<DeprecatedString, DeprecatedString>* current_group = nullptr; 93 94 auto buffer = TRY(ByteBuffer::create_uninitialized(4096)); 95 while (TRY(m_file->can_read_line())) { 96 auto line = TRY(m_file->read_line(buffer)); 97 size_t i = 0; 98 99 while (i < line.length() && (line[i] == ' ' || line[i] == '\t' || line[i] == '\n')) 100 ++i; 101 102 if (i >= line.length()) 103 continue; 104 105 switch (line[i]) { 106 case '#': // Comment, skip entire line. 107 case ';': // -||- 108 continue; 109 case '[': { // Start of new group. 110 StringBuilder builder; 111 ++i; // Skip the '[' 112 while (i < line.length() && (line[i] != ']')) { 113 builder.append(line[i]); 114 ++i; 115 } 116 current_group = &m_groups.ensure(builder.to_deprecated_string()); 117 break; 118 } 119 default: { // Start of key 120 StringBuilder key_builder; 121 StringBuilder value_builder; 122 while (i < line.length() && (line[i] != '=')) { 123 key_builder.append(line[i]); 124 ++i; 125 } 126 ++i; // Skip the '=' 127 while (i < line.length() && (line[i] != '\n')) { 128 value_builder.append(line[i]); 129 ++i; 130 } 131 if (!current_group) { 132 // We're not in a group yet, create one with the name ""... 133 current_group = &m_groups.ensure(""); 134 } 135 auto value_string = value_builder.to_deprecated_string(); 136 current_group->set(key_builder.to_deprecated_string(), value_string.trim_whitespace(TrimMode::Right)); 137 } 138 } 139 } 140 return {}; 141} 142 143DeprecatedString ConfigFile::read_entry(DeprecatedString const& group, DeprecatedString const& key, DeprecatedString const& default_value) const 144{ 145 if (!has_key(group, key)) { 146 return default_value; 147 } 148 auto it = m_groups.find(group); 149 auto jt = it->value.find(key); 150 return jt->value; 151} 152 153bool ConfigFile::read_bool_entry(DeprecatedString const& group, DeprecatedString const& key, bool default_value) const 154{ 155 auto value = read_entry(group, key, default_value ? "true" : "false"); 156 return value == "1" || value.equals_ignoring_ascii_case("true"sv); 157} 158 159void ConfigFile::write_entry(DeprecatedString const& group, DeprecatedString const& key, DeprecatedString const& value) 160{ 161 m_groups.ensure(group).ensure(key) = value; 162 m_dirty = true; 163} 164 165void ConfigFile::write_bool_entry(DeprecatedString const& group, DeprecatedString const& key, bool value) 166{ 167 write_entry(group, key, value ? "true" : "false"); 168} 169 170ErrorOr<void> ConfigFile::sync() 171{ 172 if (!m_dirty) 173 return {}; 174 175 if (!m_file) 176 return Error::from_errno(ENOENT); 177 178 TRY(m_file->truncate(0)); 179 TRY(m_file->seek(0, SeekMode::SetPosition)); 180 181 for (auto& it : m_groups) { 182 TRY(m_file->write_until_depleted(DeprecatedString::formatted("[{}]\n", it.key).bytes())); 183 for (auto& jt : it.value) 184 TRY(m_file->write_until_depleted(DeprecatedString::formatted("{}={}\n", jt.key, jt.value).bytes())); 185 TRY(m_file->write_until_depleted("\n"sv.bytes())); 186 } 187 188 m_dirty = false; 189 return {}; 190} 191 192void ConfigFile::dump() const 193{ 194 for (auto& it : m_groups) { 195 outln("[{}]", it.key); 196 for (auto& jt : it.value) 197 outln("{}={}", jt.key, jt.value); 198 outln(); 199 } 200} 201 202Vector<DeprecatedString> ConfigFile::groups() const 203{ 204 return m_groups.keys(); 205} 206 207Vector<DeprecatedString> ConfigFile::keys(DeprecatedString const& group) const 208{ 209 auto it = m_groups.find(group); 210 if (it == m_groups.end()) 211 return {}; 212 return it->value.keys(); 213} 214 215bool ConfigFile::has_key(DeprecatedString const& group, DeprecatedString const& key) const 216{ 217 auto it = m_groups.find(group); 218 if (it == m_groups.end()) 219 return {}; 220 return it->value.contains(key); 221} 222 223bool ConfigFile::has_group(DeprecatedString const& group) const 224{ 225 return m_groups.contains(group); 226} 227 228void ConfigFile::add_group(DeprecatedString const& group) 229{ 230 m_groups.ensure(group); 231 m_dirty = true; 232} 233 234void ConfigFile::remove_group(DeprecatedString const& group) 235{ 236 m_groups.remove(group); 237 m_dirty = true; 238} 239 240void ConfigFile::remove_entry(DeprecatedString const& group, DeprecatedString const& key) 241{ 242 auto it = m_groups.find(group); 243 if (it == m_groups.end()) 244 return; 245 it->value.remove(key); 246 m_dirty = true; 247} 248 249}