2012-11-01 15:19:01 +00:00
|
|
|
// Copyright (C) 2003 Dolphin Project.
|
|
|
|
|
|
|
|
// 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
|
2012-11-04 22:01:49 +00:00
|
|
|
// the Free Software Foundation, version 2.0 or later versions.
|
2012-11-01 15:19:01 +00:00
|
|
|
|
|
|
|
// 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 2.0 for more details.
|
|
|
|
|
|
|
|
// A copy of the GPL 2.0 should have been included with the program.
|
|
|
|
// If not, see http://www.gnu.org/licenses/
|
|
|
|
|
|
|
|
// Official SVN repository and contact information can be found at
|
|
|
|
// http://code.google.com/p/dolphin-emu/
|
|
|
|
|
2020-08-10 02:20:07 +00:00
|
|
|
#include <cstring>
|
2020-09-29 10:19:22 +00:00
|
|
|
|
|
|
|
#include "ppsspp_config.h"
|
|
|
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
#include <windows.h>
|
|
|
|
#undef min
|
|
|
|
#undef max
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#if PPSSPP_PLATFORM(SWITCH)
|
|
|
|
#define _GNU_SOURCE
|
|
|
|
#include <cstdio>
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#include <cstdarg>
|
|
|
|
|
|
|
|
#include <errno.h>
|
|
|
|
|
|
|
|
#include <string>
|
|
|
|
#include <sstream>
|
|
|
|
#include <limits.h>
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
#include <iomanip>
|
|
|
|
|
2020-09-29 11:02:02 +00:00
|
|
|
#include "Common/Buffer.h"
|
|
|
|
#include "Common/StringUtils.h"
|
2012-11-01 15:19:01 +00:00
|
|
|
|
2022-09-01 09:55:14 +00:00
|
|
|
size_t truncate_cpy(char *dest, size_t destSize, const char *src) {
|
2015-10-10 09:27:28 +00:00
|
|
|
size_t len = strlen(src);
|
|
|
|
if (len >= destSize - 1) {
|
|
|
|
memcpy(dest, src, destSize - 1);
|
2022-09-01 09:55:14 +00:00
|
|
|
len = destSize - 1;
|
2015-10-10 09:27:28 +00:00
|
|
|
} else {
|
|
|
|
memcpy(dest, src, len);
|
|
|
|
}
|
2022-09-01 09:55:14 +00:00
|
|
|
dest[len] = '\0';
|
|
|
|
return len;
|
2015-10-10 09:27:28 +00:00
|
|
|
}
|
|
|
|
|
2024-01-19 12:44:49 +00:00
|
|
|
size_t truncate_cpy(char *dest, size_t destSize, std::string_view src) {
|
|
|
|
if (src.size() > destSize - 1) {
|
|
|
|
memcpy(dest, src.data(), destSize - 1);
|
|
|
|
dest[destSize - 1] = 0;
|
|
|
|
return destSize - 1;
|
|
|
|
} else {
|
|
|
|
memcpy(dest, src.data(), src.size());
|
|
|
|
dest[src.size()] = 0;
|
|
|
|
return src.size();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-06 04:22:38 +00:00
|
|
|
const char* safe_string(const char* s) {
|
|
|
|
return s ? s : "(null)";
|
|
|
|
}
|
|
|
|
|
2023-12-14 11:21:11 +00:00
|
|
|
long parseHexLong(const std::string &s) {
|
2013-05-18 22:04:01 +00:00
|
|
|
long value = 0;
|
|
|
|
|
|
|
|
if (s.substr(0,2) == "0x") {
|
|
|
|
//s = s.substr(2);
|
|
|
|
}
|
|
|
|
value = strtoul(s.c_str(),0, 0);
|
|
|
|
return value;
|
|
|
|
}
|
2020-08-16 11:41:16 +00:00
|
|
|
|
2013-05-18 22:04:01 +00:00
|
|
|
long parseLong(std::string s) {
|
|
|
|
long value = 0;
|
|
|
|
if (s.substr(0,2) == "0x") {
|
|
|
|
s = s.substr(2);
|
|
|
|
value = strtol(s.c_str(),NULL, 16);
|
|
|
|
} else {
|
|
|
|
value = strtol(s.c_str(),NULL, 10);
|
|
|
|
}
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
2012-11-01 15:19:01 +00:00
|
|
|
bool CharArrayFromFormatV(char* out, int outsize, const char* format, va_list args)
|
|
|
|
{
|
|
|
|
int writtenCount = vsnprintf(out, outsize, format, args);
|
|
|
|
|
|
|
|
if (writtenCount > 0 && writtenCount < outsize)
|
|
|
|
{
|
|
|
|
out[writtenCount] = '\0';
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
out[outsize - 1] = '\0';
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool SplitPath(const std::string& full_path, std::string* _pPath, std::string* _pFilename, std::string* _pExtension)
|
|
|
|
{
|
|
|
|
if (full_path.empty())
|
|
|
|
return false;
|
|
|
|
|
|
|
|
size_t dir_end = full_path.find_last_of("/"
|
|
|
|
// windows needs the : included for something like just "C:" to be considered a directory
|
|
|
|
#ifdef _WIN32
|
|
|
|
":"
|
|
|
|
#endif
|
|
|
|
);
|
|
|
|
if (std::string::npos == dir_end)
|
|
|
|
dir_end = 0;
|
|
|
|
else
|
|
|
|
dir_end += 1;
|
|
|
|
|
|
|
|
size_t fname_end = full_path.rfind('.');
|
|
|
|
if (fname_end < dir_end || std::string::npos == fname_end)
|
|
|
|
fname_end = full_path.size();
|
|
|
|
|
|
|
|
if (_pPath)
|
|
|
|
*_pPath = full_path.substr(0, dir_end);
|
|
|
|
|
|
|
|
if (_pFilename)
|
|
|
|
*_pFilename = full_path.substr(dir_end, fname_end - dir_end);
|
|
|
|
|
|
|
|
if (_pExtension)
|
|
|
|
*_pExtension = full_path.substr(fname_end);
|
|
|
|
|
|
|
|
return true;
|
2019-02-10 14:55:00 +00:00
|
|
|
}
|
|
|
|
|
2020-09-29 10:19:22 +00:00
|
|
|
std::string LineNumberString(const std::string &str) {
|
|
|
|
std::stringstream input(str);
|
|
|
|
std::stringstream output;
|
|
|
|
std::string line;
|
|
|
|
|
|
|
|
int lineNumber = 1;
|
|
|
|
while (std::getline(input, line)) {
|
|
|
|
output << std::setw(4) << lineNumber++ << ": " << line << std::endl;
|
|
|
|
}
|
|
|
|
|
|
|
|
return output.str();
|
|
|
|
}
|
|
|
|
|
2021-02-22 02:41:08 +00:00
|
|
|
std::string IndentString(const std::string &str, const std::string &sep, bool skipFirst) {
|
|
|
|
std::stringstream input(str);
|
|
|
|
std::stringstream output;
|
|
|
|
std::string line;
|
|
|
|
|
|
|
|
bool doIndent = !skipFirst;
|
|
|
|
while (std::getline(input, line)) {
|
|
|
|
if (doIndent) {
|
|
|
|
output << sep;
|
|
|
|
}
|
|
|
|
doIndent = true;
|
|
|
|
output << line << "\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
return output.str();
|
|
|
|
}
|
|
|
|
|
2024-09-01 23:05:55 +00:00
|
|
|
std::string_view StripPrefix(std::string_view prefix, std::string_view s) {
|
|
|
|
if (startsWith(s, prefix)) {
|
|
|
|
return s.substr(prefix.size(), s.size() - prefix.size());
|
|
|
|
} else {
|
|
|
|
return s;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-29 10:19:22 +00:00
|
|
|
void SkipSpace(const char **ptr) {
|
|
|
|
while (**ptr && isspace(**ptr)) {
|
|
|
|
(*ptr)++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void DataToHexString(const uint8_t *data, size_t size, std::string *output) {
|
|
|
|
Buffer buffer;
|
|
|
|
for (size_t i = 0; i < size; i++) {
|
|
|
|
if (i && !(i & 15))
|
|
|
|
buffer.Printf("\n");
|
|
|
|
buffer.Printf("%02x ", data[i]);
|
|
|
|
}
|
|
|
|
buffer.TakeAll(output);
|
|
|
|
}
|
|
|
|
|
2021-04-18 23:14:28 +00:00
|
|
|
void DataToHexString(int indent, uint32_t startAddr, const uint8_t* data, size_t size, std::string* output) {
|
2020-09-29 10:19:22 +00:00
|
|
|
Buffer buffer;
|
|
|
|
size_t i = 0;
|
|
|
|
for (; i < size; i++) {
|
|
|
|
if (i && !(i & 15)) {
|
|
|
|
buffer.Printf(" ");
|
|
|
|
for (size_t j = i - 16; j < i; j++) {
|
|
|
|
buffer.Printf("%c", ((data[j] < 0x20) || (data[j] > 0x7e)) ? 0x2e : data[j]);
|
|
|
|
}
|
|
|
|
buffer.Printf("\n");
|
|
|
|
}
|
|
|
|
if (!(i & 15))
|
2021-04-18 23:14:28 +00:00
|
|
|
buffer.Printf("%*s%08x ", indent, "", startAddr + i);
|
2020-09-29 10:19:22 +00:00
|
|
|
buffer.Printf("%02x ", data[i]);
|
|
|
|
}
|
|
|
|
if (size & 15) {
|
|
|
|
size_t padded_size = ((size - 1) | 15) + 1;
|
|
|
|
for (size_t j = size; j < padded_size; j++) {
|
|
|
|
buffer.Printf(" ");
|
|
|
|
}
|
2021-04-18 22:48:37 +00:00
|
|
|
}
|
|
|
|
if (size > 0) {
|
2020-09-29 10:19:22 +00:00
|
|
|
buffer.Printf(" ");
|
2021-04-18 22:48:37 +00:00
|
|
|
for (size_t j = (size - 1ULL) & ~UINT64_C(0xF); j < size; j++) {
|
2020-09-29 10:19:22 +00:00
|
|
|
buffer.Printf("%c", ((data[j] < 0x20) || (data[j] > 0x7e)) ? 0x2e : data[j]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
buffer.TakeAll(output);
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string StringFromFormat(const char* format, ...)
|
|
|
|
{
|
|
|
|
va_list args;
|
|
|
|
std::string temp = "";
|
|
|
|
#ifdef _WIN32
|
|
|
|
int required = 0;
|
|
|
|
|
|
|
|
va_start(args, format);
|
|
|
|
required = _vscprintf(format, args);
|
|
|
|
// Using + 2 to be safe between MSVC versions.
|
|
|
|
// In MSVC 2015 and later, vsnprintf counts the trailing zero (per c++11.)
|
|
|
|
temp.resize(required + 2);
|
|
|
|
if (vsnprintf(&temp[0], required + 1, format, args) < 0) {
|
|
|
|
temp.resize(0);
|
|
|
|
} else {
|
|
|
|
temp.resize(required);
|
|
|
|
}
|
|
|
|
va_end(args);
|
|
|
|
#else
|
|
|
|
char *buf = nullptr;
|
|
|
|
|
|
|
|
va_start(args, format);
|
|
|
|
if (vasprintf(&buf, format, args) < 0)
|
|
|
|
buf = nullptr;
|
|
|
|
va_end(args);
|
|
|
|
|
|
|
|
if (buf != nullptr) {
|
|
|
|
temp = buf;
|
|
|
|
free(buf);
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
return temp;
|
|
|
|
}
|
|
|
|
|
2023-09-26 11:19:19 +00:00
|
|
|
std::string StringFromInt(int value) {
|
2020-09-29 10:19:22 +00:00
|
|
|
char temp[16];
|
2023-04-28 19:04:05 +00:00
|
|
|
snprintf(temp, sizeof(temp), "%d", value);
|
2020-09-29 10:19:22 +00:00
|
|
|
return temp;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Turns " hej " into "hej". Also handles tabs.
|
2023-09-26 11:19:19 +00:00
|
|
|
std::string StripSpaces(const std::string &str) {
|
2020-09-29 10:19:22 +00:00
|
|
|
const size_t s = str.find_first_not_of(" \t\r\n");
|
|
|
|
if (str.npos != s)
|
|
|
|
return str.substr(s, str.find_last_not_of(" \t\r\n") - s + 1);
|
|
|
|
else
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
// "\"hello\"" is turned to "hello"
|
|
|
|
// This one assumes that the string has already been space stripped in both
|
|
|
|
// ends, as done by StripSpaces above, for example.
|
|
|
|
std::string StripQuotes(const std::string& s)
|
|
|
|
{
|
|
|
|
if (s.size() && '\"' == s[0] && '\"' == *s.rbegin())
|
|
|
|
return s.substr(1, s.size() - 2);
|
|
|
|
else
|
|
|
|
return s;
|
|
|
|
}
|
|
|
|
|
2023-09-26 11:19:19 +00:00
|
|
|
// Turns " hej " into "hej". Also handles tabs.
|
|
|
|
std::string_view StripSpaces(std::string_view str) {
|
|
|
|
const size_t s = str.find_first_not_of(" \t\r\n");
|
|
|
|
if (str.npos != s)
|
|
|
|
return str.substr(s, str.find_last_not_of(" \t\r\n") - s + 1);
|
|
|
|
else
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
// "\"hello\"" is turned to "hello"
|
|
|
|
// This one assumes that the string has already been space stripped in both
|
|
|
|
// ends, as done by StripSpaces above, for example.
|
|
|
|
std::string_view StripQuotes(std::string_view s) {
|
|
|
|
if (s.size() && '\"' == s[0] && '\"' == *s.rbegin())
|
|
|
|
return s.substr(1, s.size() - 2);
|
|
|
|
else
|
|
|
|
return s;
|
|
|
|
}
|
|
|
|
|
2024-01-12 11:34:47 +00:00
|
|
|
// NOTE: str must live at least as long as all uses of output.
|
2023-11-13 22:36:34 +00:00
|
|
|
void SplitString(std::string_view str, const char delim, std::vector<std::string_view> &output) {
|
|
|
|
size_t next = 0;
|
|
|
|
for (size_t pos = 0, len = str.length(); pos < len; ++pos) {
|
|
|
|
if (str[pos] == delim) {
|
|
|
|
output.emplace_back(str.substr(next, pos - next));
|
|
|
|
// Skip the delimiter itself.
|
|
|
|
next = pos + 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (next == 0) {
|
|
|
|
output.push_back(str);
|
|
|
|
} else if (next < str.length()) {
|
|
|
|
output.emplace_back(str.substr(next));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-13 22:24:42 +00:00
|
|
|
void SplitString(std::string_view str, const char delim, std::vector<std::string> &output) {
|
2020-09-29 10:19:22 +00:00
|
|
|
size_t next = 0;
|
2024-04-02 14:01:14 +00:00
|
|
|
size_t pos = 0;
|
|
|
|
while (pos < str.length()) {
|
|
|
|
size_t delimPos = str.find(delim, pos);
|
|
|
|
if (delimPos == std::string_view::npos) {
|
|
|
|
break;
|
2020-09-29 10:19:22 +00:00
|
|
|
}
|
2024-04-02 14:01:14 +00:00
|
|
|
output.emplace_back(str.substr(next, delimPos - next));
|
|
|
|
next = delimPos + 1;
|
|
|
|
pos = delimPos + 1;
|
2020-09-29 10:19:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (next == 0) {
|
2023-11-13 22:36:34 +00:00
|
|
|
output.emplace_back(str);
|
2020-09-29 10:19:22 +00:00
|
|
|
} else if (next < str.length()) {
|
2022-09-30 09:35:28 +00:00
|
|
|
output.emplace_back(str.substr(next));
|
2020-09-29 10:19:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-05 09:58:35 +00:00
|
|
|
static std::string ApplyHtmlEscapes(std::string_view str_view) {
|
2022-11-05 22:31:30 +00:00
|
|
|
struct Repl {
|
|
|
|
const char *a;
|
|
|
|
const char *b;
|
|
|
|
};
|
|
|
|
|
|
|
|
static const Repl replacements[] = {
|
|
|
|
{ "&", "&" },
|
|
|
|
// Easy to add more cases.
|
|
|
|
};
|
|
|
|
|
2024-05-05 09:58:35 +00:00
|
|
|
std::string str(str_view);
|
2022-11-05 22:31:30 +00:00
|
|
|
for (const Repl &r : replacements) {
|
|
|
|
str = ReplaceAll(str, r.a, r.b);
|
|
|
|
}
|
|
|
|
return str;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Meant for HTML listings and similar, so supports some HTML escapes.
|
2024-05-05 09:58:35 +00:00
|
|
|
void GetQuotedStrings(std::string_view str, std::vector<std::string> &output) {
|
2020-09-29 10:19:22 +00:00
|
|
|
size_t next = 0;
|
|
|
|
bool even = 0;
|
|
|
|
for (size_t pos = 0, len = str.length(); pos < len; ++pos) {
|
|
|
|
if (str[pos] == '\"' || str[pos] == '\'') {
|
|
|
|
if (even) {
|
|
|
|
//quoted text
|
2022-11-05 22:31:30 +00:00
|
|
|
output.emplace_back(ApplyHtmlEscapes(str.substr(next, pos - next)));
|
2020-09-29 10:19:22 +00:00
|
|
|
even = 0;
|
|
|
|
} else {
|
|
|
|
//non quoted text
|
|
|
|
even = 1;
|
|
|
|
}
|
|
|
|
// Skip the delimiter itself.
|
|
|
|
next = pos + 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-05 09:58:35 +00:00
|
|
|
// TODO: this is quite inefficient.
|
2023-12-29 07:07:23 +00:00
|
|
|
std::string ReplaceAll(std::string_view input, std::string_view src, std::string_view dest) {
|
2020-09-29 10:19:22 +00:00
|
|
|
size_t pos = 0;
|
|
|
|
|
2023-12-29 07:07:23 +00:00
|
|
|
std::string result(input);
|
2020-09-29 10:19:22 +00:00
|
|
|
if (src == dest)
|
|
|
|
return result;
|
|
|
|
|
2023-12-29 07:07:23 +00:00
|
|
|
// TODO: Don't mutate the input, just append stuff to the output instead.
|
2023-11-13 22:36:34 +00:00
|
|
|
while (true) {
|
2020-09-29 10:19:22 +00:00
|
|
|
pos = result.find(src, pos);
|
|
|
|
if (pos == result.npos)
|
|
|
|
break;
|
|
|
|
result.replace(pos, src.size(), dest);
|
|
|
|
pos += dest.size();
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
2023-04-23 12:36:34 +00:00
|
|
|
|
2024-01-19 12:44:49 +00:00
|
|
|
std::string UnescapeMenuString(std::string_view input, char *shortcutChar) {
|
|
|
|
size_t len = input.length();
|
2023-04-23 12:36:34 +00:00
|
|
|
std::string output;
|
|
|
|
output.reserve(len);
|
|
|
|
bool escaping = false;
|
2023-04-23 12:42:36 +00:00
|
|
|
bool escapeFound = false;
|
2023-04-23 12:36:34 +00:00
|
|
|
for (size_t i = 0; i < len; i++) {
|
|
|
|
if (input[i] == '&') {
|
|
|
|
if (escaping) {
|
|
|
|
output.push_back(input[i]);
|
|
|
|
escaping = false;
|
|
|
|
} else {
|
|
|
|
escaping = true;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
output.push_back(input[i]);
|
2023-04-23 12:42:36 +00:00
|
|
|
if (escaping && shortcutChar && !escapeFound) {
|
2023-04-23 12:36:34 +00:00
|
|
|
*shortcutChar = input[i];
|
2023-04-23 12:42:36 +00:00
|
|
|
escapeFound = true;
|
2023-04-23 12:36:34 +00:00
|
|
|
}
|
|
|
|
escaping = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return output;
|
2023-04-28 19:04:05 +00:00
|
|
|
}
|
2023-07-16 14:16:47 +00:00
|
|
|
|
2024-01-19 12:44:49 +00:00
|
|
|
std::string ApplySafeSubstitutions(std::string_view format, std::string_view string1, std::string_view string2, std::string_view string3, std::string_view string4) {
|
|
|
|
size_t formatLen = format.length();
|
2023-07-16 14:16:47 +00:00
|
|
|
std::string output;
|
|
|
|
output.reserve(formatLen + 20);
|
|
|
|
for (size_t i = 0; i < formatLen; i++) {
|
|
|
|
char c = format[i];
|
|
|
|
if (c != '%') {
|
|
|
|
output.push_back(c);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (i >= formatLen - 1) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
switch (format[i + 1]) {
|
|
|
|
case '1':
|
|
|
|
output += string1; i++;
|
|
|
|
break;
|
|
|
|
case '2':
|
|
|
|
output += string2; i++;
|
|
|
|
break;
|
|
|
|
case '3':
|
|
|
|
output += string3; i++;
|
|
|
|
break;
|
2023-10-04 12:36:42 +00:00
|
|
|
case '4':
|
|
|
|
output += string4; i++;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return output;
|
|
|
|
}
|
|
|
|
|
2024-01-19 12:44:49 +00:00
|
|
|
std::string ApplySafeSubstitutions(std::string_view format, int i1, int i2, int i3, int i4) {
|
|
|
|
size_t formatLen = format.length();
|
2023-10-04 12:36:42 +00:00
|
|
|
std::string output;
|
|
|
|
output.reserve(formatLen + 20);
|
|
|
|
for (size_t i = 0; i < formatLen; i++) {
|
|
|
|
char c = format[i];
|
|
|
|
if (c != '%') {
|
|
|
|
output.push_back(c);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (i >= formatLen - 1) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
switch (format[i + 1]) {
|
|
|
|
case '1':
|
|
|
|
output += StringFromInt(i1); i++;
|
|
|
|
break;
|
|
|
|
case '2':
|
|
|
|
output += StringFromInt(i2); i++;
|
|
|
|
break;
|
|
|
|
case '3':
|
|
|
|
output += StringFromInt(i3); i++;
|
|
|
|
break;
|
|
|
|
case '4':
|
|
|
|
output += StringFromInt(i4); i++;
|
|
|
|
break;
|
2023-07-16 14:16:47 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return output;
|
|
|
|
}
|