Compare commits

...

8 Commits

13 changed files with 419 additions and 8 deletions

View File

@ -25,3 +25,4 @@ luna_app(mkdir.cpp mkdir OFF)
luna_app(rm.cpp rm OFF)
luna_app(stat.cpp stat OFF)
luna_app(uname.cpp uname OFF)
luna_app(base64.cpp base64 OFF)

41
apps/base64.cpp Normal file
View File

@ -0,0 +1,41 @@
#include <luna/Base64.h>
#include <os/ArgumentParser.h>
#include <os/File.h>
Result<int> luna_main(int argc, char** argv)
{
StringView path;
bool decode { false };
bool allow_garbage { false };
os::ArgumentParser parser;
parser.add_description("Encode or decode Base64 data. If not given a file, reads from standard input.");
parser.add_positional_argument(path, "file", "-"_sv);
parser.add_switch_argument(decode, 'd', "decode", "decode data");
parser.add_switch_argument(allow_garbage, ' ', "allow-garbage", "ignore non-base64 characters");
parser.parse(argc, argv);
auto file = TRY(os::File::open_input_file(path));
auto output = os::File::standard_output();
if (!decode)
{
auto data = TRY(file->read_all());
auto encoded = TRY(Base64::encode(data));
output->write(encoded.view());
output->write("\n"_sv);
}
else
{
auto data = TRY(file->read_all_as_string());
auto decoded = TRY(Base64::decode(data.view(), allow_garbage));
output->write(decoded);
}
return 0;
}

View File

@ -6,14 +6,10 @@ using os::File;
static Result<void> do_cat(StringView path)
{
SharedPtr<File> f;
SharedPtr<File> f = TRY(File::open_input_file(path));
auto out = File::standard_output();
if (path == "-") f = File::standard_input();
else
f = TRY(File::open(path, File::ReadOnly));
auto buf = TRY(Buffer::create_sized(4096));
while (1)
{

View File

@ -59,7 +59,7 @@ void ConsoleDevice::did_press_key(char key)
// Ctrl+D
if (key == 'd' && (Keyboard::modifiers() & Keyboard::LeftControl))
{
g_eof = true;
if (g_temp_input.size() == 0) g_eof = true;
return;
}

View File

@ -2,6 +2,7 @@
#include "boot/bootboot.h"
#include "video/Framebuffer.h"
#include <luna/CString.h>
#include <luna/CType.h>
#include <luna/Format.h>
#include <luna/Result.h>
#include <luna/ScopeGuard.h>
@ -109,6 +110,7 @@ namespace TextConsole
}
break;
default: {
if (_iscntrl(c)) return;
putwchar_at(c, g_x_position, g_y_position);
next_char();
if (at_end_of_screen())

View File

@ -23,6 +23,7 @@ set(FREESTANDING_SOURCES
src/Spinlock.cpp
src/PathParser.cpp
src/UBSAN.cpp
src/Base64.cpp
)
set(SOURCES

View File

@ -0,0 +1,13 @@
#pragma once
#include <luna/Buffer.h>
#include <luna/String.h>
namespace Base64
{
Result<String> encode(StringView data);
Result<String> encode(Slice<const u8> data);
Result<String> encode(const Buffer& data);
Result<Buffer> decode(StringView data, bool allow_garbage_chars = false);
Result<String> decode_string(StringView data, bool allow_garbage_chars = false);
}

View File

@ -11,6 +11,10 @@ template <typename T> class Slice
{
}
template <typename Tp> Slice(const Slice<Tp>& other) : m_data((T*)other.m_data), m_size(other.m_size)
{
}
const T* data() const
{
return m_data;
@ -46,6 +50,18 @@ template <typename T> class Slice
return m_data + m_size;
}
const T& operator[](usize index) const
{
check(index < m_size);
return m_data[index];
}
T& operator[](usize index)
{
check(index < m_size);
return m_data[index];
}
private:
T* const m_data;
const usize m_size;

144
libluna/src/Base64.cpp Normal file
View File

@ -0,0 +1,144 @@
#include <luna/Base64.h>
#include <luna/CType.h>
#include <luna/DebugLog.h>
#include <luna/Slice.h>
#include <luna/StringBuilder.h>
static const char g_base64_alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
static Option<u8> value_from_base64_character(char letter)
{
if (_isupper(letter)) return (u8)(letter - 'A');
if (_islower(letter)) return (u8)(26 + (letter - 'a'));
if (_isdigit(letter)) return (u8)(52 + (letter - '0'));
if (letter == '+') return 62;
if (letter == '/') return 63;
if (letter == '=') return {};
unreachable();
}
static Result<void> decode_base64_buffer(const Vector<char>& input, Buffer& output)
{
const usize bytes_to_decode = 4;
Option<u8> base64_data[bytes_to_decode];
for (usize i = 0; i < 4; i++) { base64_data[i] = value_from_base64_character(input[i]); }
u8 decoded_bytes[3];
usize total_decoded_bytes = 0;
decoded_bytes[0] = (base64_data[0].value() << 2) | (base64_data[1].value() >> 4);
total_decoded_bytes++;
if (base64_data[2].has_value())
{
decoded_bytes[1] = ((base64_data[1].value() & 0b1111) << 4) | (base64_data[2].value() >> 2);
total_decoded_bytes++;
}
if (base64_data[3].has_value())
{
decoded_bytes[2] = ((base64_data[2].value() & 0b11) << 6) | base64_data[3].value();
total_decoded_bytes++;
}
return output.append_data(decoded_bytes, total_decoded_bytes);
}
namespace Base64
{
Result<String> encode(StringView data)
{
return encode(Slice<const u8> { (const u8*)data.chars(), data.length() });
}
Result<String> encode(const Buffer& data)
{
return encode(Slice<const u8> { data.data(), data.size() });
}
Result<String> encode(Slice<const u8> data)
{
StringBuilder sb;
for (usize i = 0; i < data.size(); i += 3)
{
usize bytes_to_encode = data.size() - i;
if (bytes_to_encode > 3) bytes_to_encode = 3;
TRY(sb.add(g_base64_alphabet[data[i] >> 2]));
if (bytes_to_encode > 1) TRY(sb.add(g_base64_alphabet[((data[i] & 0b11) << 4) | (data[i + 1] >> 4)]));
else
{
TRY(sb.add(g_base64_alphabet[(data[i] & 0b11) << 6]));
TRY(sb.add("=="_sv));
break;
}
if (bytes_to_encode > 2) TRY(sb.add(g_base64_alphabet[((data[i + 1] & 0b1111) << 2) | (data[i + 2] >> 6)]));
else
{
TRY(sb.add(g_base64_alphabet[(data[i + 1] & 0b1111) << 2]));
TRY(sb.add("="_sv));
break;
}
TRY(sb.add(g_base64_alphabet[data[i + 2] & 0b111111]));
}
return sb.string();
}
Result<Buffer> decode(StringView data, bool allow_garbage_chars)
{
Buffer buf;
char* padding = strchr(data.chars(), '=');
if (padding)
{
// If the string ends with padding, it must be either one or two equals signs.
if (padding[1] != '=' && padding[1] != '\0') return err(EINVAL);
if (padding[1])
{
if (padding[strspn(&padding[2], "\n") + 2]) return err(EINVAL);
}
}
Vector<char> chars_read;
for (const auto& c : data)
{
if (c == '\n') continue;
if (!_isalnum(c) && c != '+' && c != '/' && c != '=')
{
if (allow_garbage_chars) continue;
return err(EINVAL);
}
TRY(chars_read.try_append(c));
if (chars_read.size() == 4)
{
TRY(decode_base64_buffer(chars_read, buf));
chars_read.clear();
}
}
// Unterminated input
if (chars_read.size() > 0) return err(EINVAL);
return buf;
}
Result<String> decode_string(StringView data, bool allow_garbage_chars)
{
auto buf = TRY(decode(data, allow_garbage_chars));
u8 nul_byte = '\0';
TRY(buf.append_data(&nul_byte, 1));
return String::from_cstring((char*)buf.data());
}
}

View File

@ -24,6 +24,14 @@ namespace os
static Result<SharedPtr<File>> open_or_create(StringView path, OpenMode flags, mode_t mode = 0644);
static Result<SharedPtr<File>> create(StringView path, OpenMode flags, mode_t mode = 0644);
/*
If path is "-", return standard input (as is common for many CLI apps). Otherwise, open path for reading.
This function is a convenience function for CLI apps, so that they don't need to check path and open standard
input if necessary.
*/
static Result<SharedPtr<File>> open_input_file(StringView path);
static SharedPtr<File> standard_input();
static SharedPtr<File> standard_output();
static SharedPtr<File> standard_error();
@ -34,8 +42,10 @@ namespace os
Result<void> write(const Buffer& buf);
Result<String> read_line();
Result<String> read_all_as_string();
Result<void> read(Buffer& buf, usize size);
Result<Buffer> read_all();
Result<int> getchar();

View File

@ -82,6 +82,13 @@ namespace os
return construct(path, (int)flags | (O_CREAT | O_EXCL), mode);
}
Result<SharedPtr<File>> File::open_input_file(StringView path)
{
if (path == "-"_sv) return standard_input();
return construct(path, O_RDONLY, 0);
}
Result<usize> File::raw_read(u8* buf, usize length)
{
long rc = syscall(SYS_read, m_fd, buf, length);
@ -131,6 +138,36 @@ namespace os
return String::from_cstring(data.data());
}
Result<String> File::read_all_as_string()
{
StringBuilder sb;
while (true)
{
auto line = TRY(read_line());
if (line.is_empty()) break;
TRY(sb.add(line));
}
return sb.string();
}
Result<Buffer> File::read_all()
{
Vector<u8> data;
while (true)
{
int c = TRY(getchar());
if (c == -1) break;
TRY(data.try_append((u8)c));
}
Buffer buf;
TRY(buf.append_data(data.data(), data.size()));
return buf;
}
Result<void> File::read(Buffer& buf, usize size)
{
u8* slice = TRY(buf.slice(0, size));
@ -144,9 +181,9 @@ namespace os
Result<int> File::getchar()
{
char value;
u8 value;
usize nread = TRY(raw_read((u8*)&value, 1));
usize nread = TRY(raw_read(&value, 1));
if (!nread) return -1;
return value;

View File

@ -17,3 +17,4 @@ function(luna_test SOURCE_FILE APP_NAME SETUID)
endfunction()
luna_test(libluna/TestVector.cpp TestVector OFF)
luna_test(libluna/TestBase64.cpp TestBase64 OFF)

View File

@ -0,0 +1,149 @@
#include <luna/Base64.h>
#include <test.h>
TestResult test_base64_encode_unpadded_message()
{
auto encoded = TRY(Base64::encode("abc"_sv));
validate(encoded.view() == "YWJj");
test_success;
}
TestResult test_base64_decode_unpadded_message()
{
auto rc = Base64::decode_string("YWJj"_sv);
if (rc.has_error())
{
validate(rc.error() != EINVAL);
return rc.release_error();
}
auto decoded = rc.release_value();
validate(decoded.view() == "abc"_sv);
test_success;
}
TestResult test_base64_encode_padded_message()
{
auto encoded = TRY(Base64::encode("abcd"_sv));
validate(encoded.view() == "YWJjZA==");
test_success;
}
TestResult test_base64_encode_padded_message_2()
{
auto encoded = TRY(Base64::encode("abcde"_sv));
validate(encoded.view() == "YWJjZGU=");
test_success;
}
TestResult test_base64_decode_padded_message()
{
auto rc = Base64::decode_string("YWJjZA=="_sv);
if (rc.has_error())
{
validate(rc.error() != EINVAL);
return rc.release_error();
}
auto decoded = rc.release_value();
validate(decoded.view() == "abcd"_sv);
test_success;
}
TestResult test_base64_decode_padded_message_2()
{
auto rc = Base64::decode_string("YWJjZGU="_sv);
if (rc.has_error())
{
validate(rc.error() != EINVAL);
return rc.release_error();
}
auto decoded = rc.release_value();
validate(decoded.view() == "abcde"_sv);
test_success;
}
TestResult test_base64_decode_padded_message_with_newlines()
{
auto rc = Base64::decode_string("YWJj\nZA==\n"_sv);
if (rc.has_error())
{
validate(rc.error() != EINVAL);
return rc.release_error();
}
auto decoded = rc.release_value();
validate(decoded.view() == "abcd"_sv);
test_success;
}
TestResult test_base64_disallow_characters_after_padding()
{
auto rc = Base64::decode_string("YWJjZA==bd"_sv);
validate(rc.has_error());
if (rc.error() != EINVAL) return rc.release_error();
test_success;
}
TestResult test_base64_disallow_garbage_chars_by_default()
{
auto rc = Base64::decode_string("YWJj?-ZA=="_sv);
validate(rc.has_error());
if (rc.error() != EINVAL) return rc.release_error();
test_success;
}
TestResult test_base64_skip_garbage_chars_if_allowed()
{
auto rc = Base64::decode_string("YWJj?-ZA=="_sv, true);
if (rc.has_error())
{
validate(rc.error() != EINVAL);
return rc.release_error();
}
auto decoded = rc.release_value();
validate(decoded.view() == "abcd"_sv);
test_success;
}
Result<void> test_main()
{
test_prelude;
run_test(test_base64_encode_unpadded_message);
run_test(test_base64_decode_unpadded_message);
run_test(test_base64_encode_padded_message);
run_test(test_base64_encode_padded_message_2);
run_test(test_base64_decode_padded_message);
run_test(test_base64_decode_padded_message_2);
run_test(test_base64_decode_padded_message_with_newlines);
run_test(test_base64_disallow_characters_after_padding);
run_test(test_base64_disallow_garbage_chars_by_default);
run_test(test_base64_skip_garbage_chars_if_allowed);
return {};
}