// Copyright (C) 2026 Viorel Munteanu // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. #include "user_spec.h" #include #include #include #include #include namespace { std::vector split(const std::string& line, char delim) { std::vector fields; std::istringstream stream(line); std::string field; while (std::getline(stream, field, delim)) { fields.push_back(field); } return fields; } bool is_all_digits(const std::string& s) { return !s.empty() && std::all_of(s.begin(), s.end(), [](unsigned char c) { return std::isdigit(c); }); } std::optional parse_int(const std::string& s) { try { return std::stoi(s); } catch (const std::exception&) { return std::nullopt; } } // Looks up `key` by name (field 0) or numeric id (field `id_field`) in a // colon-separated database file (/etc/passwd or /etc/group). Malformed lines are // skipped rather than treated as errors. std::optional> lookup_entry(const std::filesystem::path& db_file, const std::string& key, size_t id_field) { std::ifstream in(db_file, std::ios::binary); if (!in) { return std::nullopt; } std::string line; while (std::getline(in, line)) { auto fields = split(line, ':'); if (fields.size() <= id_field) { continue; } if (fields[0] == key || fields[id_field] == key) { return fields; } } return std::nullopt; } } // namespace std::optional resolve_user_and_group(const std::string& user, const std::optional& group, const std::filesystem::path& image_root) { std::filesystem::path passwd_file = image_root / "etc" / "passwd"; int uid = 0; int gid = 0; if (is_all_digits(user)) { uid = *parse_int(user); auto entry = lookup_entry(passwd_file, user, 2); if (entry && entry->size() > 3) { gid = parse_int((*entry)[3]).value_or(uid); } else { gid = uid; } } else { auto entry = lookup_entry(passwd_file, user, 2); if (!entry || entry->size() <= 3) { spdlog::error("could not resolve user '{}' in the image's /etc/passwd", user); return std::nullopt; } auto entry_uid = parse_int((*entry)[2]); auto entry_gid = parse_int((*entry)[3]); if (!entry_uid || !entry_gid) { spdlog::error("malformed /etc/passwd entry for user '{}'", user); return std::nullopt; } uid = *entry_uid; gid = *entry_gid; } if (group) { if (is_all_digits(*group)) { gid = *parse_int(*group); } else { std::filesystem::path group_file = image_root / "etc" / "group"; auto entry = lookup_entry(group_file, *group, 2); auto entry_gid = entry ? parse_int((*entry)[2]) : std::nullopt; if (!entry_gid) { spdlog::error("could not resolve group '{}' in the image's /etc/group", *group); return std::nullopt; } gid = *entry_gid; } } // Looked up by the final resolved uid (field 2), independent of whether `user` // was given as a name or a number, so it matches whichever /etc/passwd row // actually owns that uid. No matching row -> fall back to "/root" for uid 0 // (matches useradd-less images' own convention for root) or "/" otherwise. std::string home = uid == 0 ? "/root" : "/"; auto home_entry = lookup_entry(passwd_file, std::to_string(uid), 2); if (home_entry && home_entry->size() > 5 && !(*home_entry)[5].empty()) { home = (*home_entry)[5]; } return ResolvedUser{uid, gid, home}; }