/*
* Proxy Pool Governor — scenario (.pps) file reader/writer.
*
* Copyright (C) 2026 SWGY, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include "scenario.h"
#include <errno.h>
#include <stdlib.h>
#include <string.h>
static int
parse_header(Scenario *s)
{
char line[1024];
int in_pools = 0, in_services = 0;
size_t expect_pools = 0, expect_services = 0;
size_t pool_count = 0, service_count = 0;
while (fgets(line, sizeof(line), s->fp) != NULL) {
/* Strip newline */
size_t len = strlen(line);
if (len > 0 && line[len-1] == '\n')
line[--len] = '\0';
if (len > 0 && line[len-1] == '\r')
line[--len] = '\0';
/* Skip empty lines and comments */
if (len == 0 || line[0] == '#')
continue;
/* Data section marker */
if (strcmp(line, "---") == 0) {
s->in_data_section = 1;
break;
}
/* Directive */
if (line[0] == '@') {
in_pools = 0;
in_services = 0;
if (strncmp(line, "@pools ", 7) == 0) {
expect_pools = (size_t)atoi(line + 7);
in_pools = 1;
pool_count = 0;
} else if (strncmp(line, "@services ", 10) == 0) {
expect_services = (size_t)atoi(line + 10);
in_services = 1;
service_count = 0;
} else if (strncmp(line, "@records ", 9) == 0) {
s->num_records = (size_t)atoi(line + 9);
}
/* Unknown directives ignored for forward compat */
continue;
}
/* ID-name mapping lines */
if (in_pools && pool_count < expect_pools) {
int id;
char name[SCENARIO_MAX_NAME];
if (sscanf(line, "%d %63s", &id, name) == 2) {
if (id >= 0 && id < SCENARIO_MAX_POOLS) {
strncpy(s->pool_names[id], name,
SCENARIO_MAX_NAME - 1);
s->pool_names[id][SCENARIO_MAX_NAME-1] = '\0';
pool_count++;
if (id >= (int)s->num_pools)
s->num_pools = id + 1;
}
}
continue;
}
if (in_services && service_count < expect_services) {
int id;
char name[SCENARIO_MAX_NAME];
if (sscanf(line, "%d %63s", &id, name) == 2) {
if (id >= 0 && id < SCENARIO_MAX_SERVICES) {
strncpy(s->service_names[id], name,
SCENARIO_MAX_NAME - 1);
s->service_names[id][SCENARIO_MAX_NAME-1] = '\0';
service_count++;
if (id >= (int)s->num_services)
s->num_services = id + 1;
}
}
continue;
}
}
if (!s->in_data_section) {
errno = EINVAL;
return -1;
}
s->header_parsed = 1;
return 0;
}
Scenario *
scenario_open_read(const char *path)
{
Scenario *s;
FILE *fp;
fp = fopen(path, "r");
if (fp == NULL)
return NULL;
s = calloc(1, sizeof(*s));
if (s == NULL) {
fclose(fp);
return NULL;
}
s->fp = fp;
s->mode = 'r';
if (parse_header(s) < 0) {
fclose(fp);
free(s);
return NULL;
}
return s;
}
Scenario *
scenario_open_write(const char *path)
{
Scenario *s;
FILE *fp;
fp = fopen(path, "w");
if (fp == NULL)
return NULL;
s = calloc(1, sizeof(*s));
if (s == NULL) {
fclose(fp);
return NULL;
}
s->fp = fp;
s->mode = 'w';
return s;
}
int
scenario_write_header(Scenario *s, size_t num_records)
{
size_t i;
if (s->mode != 'w')
return -1;
fprintf(s->fp, "# Proxy Pool Scenario v%d\n", SCENARIO_VERSION);
fprintf(s->fp, "\n");
fprintf(s->fp, "@pools %zu\n", s->num_pools);
for (i = 0; i < s->num_pools; i++)
fprintf(s->fp, "%zu %s\n", i, s->pool_names[i]);
fprintf(s->fp, "\n");
fprintf(s->fp, "@services %zu\n", s->num_services);
for (i = 0; i < s->num_services; i++)
fprintf(s->fp, "%zu %s\n", i, s->service_names[i]);
fprintf(s->fp, "\n");
if (num_records > 0)
fprintf(s->fp, "@records %zu\n", num_records);
fprintf(s->fp, "---\n");
s->in_data_section = 1;
s->num_records = num_records;
return 0;
}
int
scenario_read(Scenario *s, ScenarioRecord *rec)
{
char line[512];
int pool, service;
if (s->mode != 'r' || !s->in_data_section)
return -1;
while (fgets(line, sizeof(line), s->fp) != NULL) {
/* Skip comments and empty lines in data section */
if (line[0] == '#' || line[0] == '\n')
continue;
int n = sscanf(line,
"%lf %d %d %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf",
&rec->timestamp,
&pool,
&service,
&rec->rate_success,
&rec->rate_lost_race,
&rec->rate_302,
&rec->rate_timeout,
&rec->rate_ssl,
&rec->rate_other,
&rec->response_time,
&rec->avg_success,
&rec->avg_response_time,
&rec->stddev_success,
&rec->stddev_response_time);
if (n != 14) {
errno = EINVAL;
return -1;
}
rec->pool = (uint8_t)pool;
rec->service = (uint8_t)service;
s->records_read++;
return 1;
}
return 0; /* EOF */
}
int
scenario_write(Scenario *s, const ScenarioRecord *rec)
{
if (s->mode != 'w' || !s->in_data_section)
return -1;
fprintf(s->fp, "%.1f %d %d %.4f %.4f %.4f %.4f %.4f %.4f %.3f %.4f %.3f %.4f %.3f\n",
rec->timestamp,
rec->pool,
rec->service,
rec->rate_success,
rec->rate_lost_race,
rec->rate_302,
rec->rate_timeout,
rec->rate_ssl,
rec->rate_other,
rec->response_time,
rec->avg_success,
rec->avg_response_time,
rec->stddev_success,
rec->stddev_response_time);
return 0;
}
void
scenario_close(Scenario *s)
{
if (s == NULL)
return;
if (s->fp != NULL)
fclose(s->fp);
free(s);
}
const char *
scenario_pool_name(const Scenario *s, uint8_t id)
{
if (id >= s->num_pools)
return NULL;
return s->pool_names[id];
}
const char *
scenario_service_name(const Scenario *s, uint8_t id)
{
if (id >= s->num_services)
return NULL;
return s->service_names[id];
}
int
scenario_pool_id(const Scenario *s, const char *name)
{
for (size_t i = 0; i < s->num_pools; i++) {
if (strcmp(s->pool_names[i], name) == 0)
return (int)i;
}
return -1;
}
int
scenario_service_id(const Scenario *s, const char *name)
{
for (size_t i = 0; i < s->num_services; i++) {
if (strcmp(s->service_names[i], name) == 0)
return (int)i;
}
return -1;
}