// SPDX-License-Identifier: Apache-2.0
/*
Copyright (C) 2023 The Falco Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

*/

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif

#include <stdio.h>
#include <libscap/scap.h>
#include <libscap/scap-int.h>
#include <libscap/linux/scap_linux_platform.h>
#include <libscap/strl.h>

#include <sys/types.h>

#include <pwd.h>
#include <grp.h>
#include <libscap/strerror.h>

//
// Allocate and return the list of users on this system
//
int32_t scap_linux_create_userlist(struct scap_platform *platform) {
	struct scap_linux_platform *handle = (struct scap_linux_platform *)platform;
	bool file_lookup = false;
	FILE *f = NULL;
	char filename[SCAP_MAX_PATH_SIZE];
	uint32_t usercnt, useridx;
	uint32_t grpcnt, grpidx;
	struct passwd *p;
	struct group *g;
	struct scap_userlist *userlist;

	//
	// If the list of users was already allocated for this handle (for example because this is
	// not the first user list block), free it
	//
	if(platform->m_userlist != NULL) {
		scap_free_userlist(platform->m_userlist);
		platform->m_userlist = NULL;
	}

	//
	// Memory allocations
	//
	platform->m_userlist = (scap_userlist *)malloc(sizeof(scap_userlist));
	if(platform->m_userlist == NULL) {
		return scap_errprintf(handle->m_lasterr, 0, "userlist allocation failed(1)");
	}
	userlist = platform->m_userlist;

	userlist->totsavelen = 0;
	usercnt = 32;  // initial user count; will be realloc'd if needed
	userlist->users = (scap_userinfo *)malloc(usercnt * sizeof(scap_userinfo));
	if(userlist->users == NULL) {
		free(userlist);
		platform->m_userlist = NULL;
		return scap_errprintf(handle->m_lasterr, 0, "userlist allocation failed(2)");
	}

	grpcnt = 32;  // initial group count; will be realloc'd if needed
	userlist->groups = (scap_groupinfo *)malloc(grpcnt * sizeof(scap_groupinfo));
	if(userlist->groups == NULL) {
		free(userlist->users);
		free(userlist);
		platform->m_userlist = NULL;
		return scap_errprintf(handle->m_lasterr, 0, "grouplist allocation failed(2)");
	}

	// check for host root
	const char *host_root = scap_get_host_root();
	if(host_root[0] == '\0') {
		file_lookup = false;
	} else {
		file_lookup = true;
	}

	// users
	if(file_lookup) {
		snprintf(filename, sizeof(filename), "%s/etc/passwd", host_root);
		f = fopen(filename, "r");
		if(f == NULL) {
			// if we don't have it inside the host root, we'll proceed without a list
			free(userlist->users);
			free(userlist->groups);
			free(userlist);
			platform->m_userlist = NULL;
			return SCAP_SUCCESS;
		}
	} else {
		setpwent();
	}

	for(useridx = 0; file_lookup ? (p = fgetpwent(f)) : (p = getpwent()); useridx++) {
		if(useridx == usercnt) {
			usercnt <<= 1;
			void *tmp = realloc(userlist->users, usercnt * sizeof(scap_userinfo));
			if(tmp) {
				userlist->users = tmp;
			} else {
				free(userlist->users);
				free(userlist->groups);
				free(userlist);
				platform->m_userlist = NULL;
				if(file_lookup) {
					fclose(f);
				} else {
					endpwent();
				}
				return scap_errprintf(handle->m_lasterr, 0, "userlist allocation failed(2)");
			}
		}

		scap_userinfo *user = &userlist->users[useridx];
		user->uid = p->pw_uid;
		user->gid = p->pw_gid;

		if(p->pw_name) {
			strlcpy(user->name, p->pw_name, sizeof(user->name));
		} else {
			*user->name = '\0';
		}

		if(p->pw_dir) {
			strlcpy(user->homedir, p->pw_dir, sizeof(user->homedir));
		} else {
			*user->homedir = '\0';
		}

		if(p->pw_shell) {
			strlcpy(user->shell, p->pw_shell, sizeof(user->shell));
		} else {
			*user->shell = '\0';
		}

		userlist->totsavelen += sizeof(uint8_t) +   // type
		                        sizeof(uint32_t) +  // uid
		                        sizeof(uint32_t) +  // gid
		                        strlen(user->name) + 2 + strlen(user->homedir) + 2 +
		                        strlen(user->shell) + 2;
	}

	if(file_lookup) {
		fclose(f);
	} else {
		endpwent();
	}

	// if userIdx == 0 -> realloc with size 0 means free, and NULL is returned.
	// so, we will end up with userlist->nusers = 0 and userlist->users NULL.
	userlist->nusers = useridx;
	if(useridx < usercnt) {
		// Reduce array size
		scap_userinfo *reduced_userinfos =
		        realloc(userlist->users, useridx * sizeof(scap_userinfo));
		if(reduced_userinfos == NULL && useridx > 0) {
			free(userlist->users);
			free(userlist->groups);
			free(userlist);
			platform->m_userlist = NULL;
			return scap_errprintf(handle->m_lasterr,
			                      0,
			                      "userlist allocation while reducing array size");
		}
		userlist->users = reduced_userinfos;
	}

	// groups
	if(file_lookup) {
		snprintf(filename, sizeof(filename), "%s/etc/group", host_root);
		f = fopen(filename, "r");
		if(f == NULL) {
			// if we reached this point we had passwd but we don't have group
			free(userlist->users);
			free(userlist->groups);
			free(userlist);
			platform->m_userlist = NULL;
			return scap_errprintf(handle->m_lasterr, 0, "failed to open %s", filename);
		}
	} else {
		setgrent();
	}

	for(grpidx = 0; file_lookup ? (g = fgetgrent(f)) : (g = getgrent()); grpidx++) {
		if(grpidx == grpcnt) {
			grpcnt <<= 1;
			void *tmp = realloc(userlist->groups, grpcnt * sizeof(scap_groupinfo));
			if(tmp) {
				userlist->groups = tmp;
			} else {
				free(userlist->users);
				free(userlist->groups);
				free(userlist);
				platform->m_userlist = NULL;
				if(file_lookup) {
					fclose(f);
				} else {
					endgrent();
				}
				return scap_errprintf(handle->m_lasterr, 0, "grouplist allocation failed(2)");
			}
		}
		scap_groupinfo *group = &userlist->groups[grpidx];
		group->gid = g->gr_gid;

		if(g->gr_name) {
			strlcpy(group->name, g->gr_name, sizeof(group->name));
		} else {
			*group->name = '\0';
		}

		userlist->totsavelen += sizeof(uint8_t) +   // type
		                        sizeof(uint32_t) +  // gid
		                        strlen(group->name) + 2;
	}

	if(file_lookup) {
		fclose(f);
	} else {
		endgrent();
	}

	// if grpidx == 0 -> realloc with size 0 means free, and NULL is returned.
	// so, we will end up with userlist->ngroups = 0 and userlist->groups NULL.
	userlist->ngroups = grpidx;
	if(grpidx < grpcnt) {
		// Reduce array size
		scap_groupinfo *reduced_groups = realloc(userlist->groups, grpidx * sizeof(scap_groupinfo));
		if(reduced_groups == NULL && grpidx > 0) {
			free(userlist->users);
			free(userlist->groups);
			free(userlist);
			platform->m_userlist = NULL;
			return scap_errprintf(handle->m_lasterr, 0, "grouplist allocation failed(2)");
		}
		userlist->groups = reduced_groups;
	}
	return SCAP_SUCCESS;
}
