Skip to content

Conversation

@Veratil
Copy link
Contributor

@Veratil Veratil commented Jun 11, 2022

Throwing this out to provide non-Qt base64 encode/decode methods. Threw them in the lmms::base64 namespace for now.

Currently provided:

std::string encode(const std::string&);
std::string decode(const std::string&);
char* encode(const char*, const size_t);

If we want to keep the char* type function, then I can add the decode method for that. If not we can delete it.

Also, I'm thinking it would be better to use move semantics for returning the std::string, but I'm not too versed in that aspect and how to make it work correctly.

@sakertooth
Copy link
Contributor

sakertooth commented Jun 11, 2022

Also, I'm thinking it would be better to use move semantics for returning the std::string, but I'm not too versed in that aspect and how to make it work correctly.

Generally, I don't think you would return T&&. If T is returned, copy elision (or more specifically, RVO) should kick in.

From https://en.cppreference.com/w/cpp/language/return#Notes:
If expression is a prvalue, the result object is initialized directly by that expression. This does not involve a copy or move constructor when the types match (see [copy elision](https://en.cppreference.com/w/cpp/language/copy_elision)).

From the standard (This one had a nice example):
— A prvalue (“pure” rvalue) is an rvalue that is not an xvalue. [ Example: The result of calling a function whose return type is not a reference is a prvalue. The value of a literal such as 12, 7.3e5, or true is also a prvalue. —end example ]

@LmmsBot
Copy link

LmmsBot commented Jun 11, 2022

🤖 Hey, I'm @LmmsBot from github.com/lmms/bot and I made downloads for this pull request, click me to make them magically appear! 🎩

Linux

Windows

macOS

🤖
{"platform_name_to_artifacts": {"Linux": [{"artifact": {"title": {"title": "(AppImage)", "platform_name": "Linux"}, "link": {"link": "https://output.circle-artifacts.com/output/job/91e9e3cd-c4c4-421b-944b-ddecd4031602/artifacts/0/lmms-1.3.0-alpha.1.216+g2bd9facd8-linux-x86_64.AppImage"}}, "build_link": "https://circleci.com/gh/LMMS/lmms/17728?utm_campaign=vcs-integration-link&utm_medium=referral&utm_source=github-build-link"}], "Windows": [{"artifact": {"title": {"title": "32-bit", "platform_name": "Windows"}, "link": {"link": "https://output.circle-artifacts.com/output/job/6eb04a28-5347-497a-a671-acc71e79d15e/artifacts/0/lmms-1.3.0-alpha.1.216+g2bd9facd8-mingw-win32.exe"}}, "build_link": "https://circleci.com/gh/LMMS/lmms/17727?utm_campaign=vcs-integration-link&utm_medium=referral&utm_source=github-build-link"}, {"artifact": {"title": {"title": "64-bit", "platform_name": "Windows"}, "link": {"link": "https://output.circle-artifacts.com/output/job/d312eeea-8dcd-4c92-b8e0-98b950aff507/artifacts/0/lmms-1.3.0-alpha.1.216+g2bd9facd8-mingw-win64.exe"}}, "build_link": "https://circleci.com/gh/LMMS/lmms/17730?utm_campaign=vcs-integration-link&utm_medium=referral&utm_source=github-build-link"}], "macOS": [{"artifact": {"title": {"title": "", "platform_name": "macOS"}, "link": {"link": "https://output.circle-artifacts.com/output/job/c7ddd18f-e1d9-4013-8538-d11ceeea3b5e/artifacts/0/lmms-1.3.0-alpha.1.216+g2bd9facd8-mac10.14.dmg"}}, "build_link": "https://circleci.com/gh/LMMS/lmms/17729?utm_campaign=vcs-integration-link&utm_medium=referral&utm_source=github-build-link"}]}, "commit_sha": "29ec4bfbc8f2209d18a2550ce918ba16692f793a"}

@JohannesLorenz
Copy link
Contributor

What would be the reason to elliminate Qt methods? FWIR, this is only useful for the GUI, which uses Qt anyways?

@Veratil
Copy link
Contributor Author

Veratil commented Jun 16, 2022

What would be the reason to elliminate Qt methods? FWIR, this is only useful for the GUI, which uses Qt anyways?

Good question! Here's the reason this PR exists:

  1. It's in core, we're removing Qt from core.
  2. This was easy enough to throw together.

If base64 is unneeded in core then we just close this PR and move the file out of core folder into gui instead. 🙂

@sakertooth
Copy link
Contributor

What would be the reason to elliminate Qt methods? FWIR, this is only useful for the GUI, which uses Qt anyways?

FWIW, my sample caching PR utilizes Base64 encoding/decoding within SampleBufferV2 (eventually it will be just SampleBuffer) when loading data from Base64. As SampleBufferV2 is part of the core, this is a useful PR. This could've been part of the original Qt removal PR for simplicity's sake though.

Do note that this is one example of Base64 usage within the core. There is probably more.

@PhysSong
Copy link
Member

I suggest always returning std::string and removing code duplication using std::string_view.

@PhysSong
Copy link
Member

Well, I mean, you can merge two encode functions using std::string_view and optionally add std::string/const char*, size_t variants.

@Veratil
Copy link
Contributor Author

Veratil commented Jun 19, 2022

Well, I mean, you can merge two encode functions using std::string_view and optionally add std::string/const char*, size_t variants.

Oh! I thought you were only talking about the std::string ones. 😁

@sakertooth
Copy link
Contributor

I made some tweaks to this implementation here. Took me a while because I was constantly refactoring it and trying to get something compact. Both functions should be functional and were tested with the test vectors specified in RFC 4648 section 4.

@JohannesLorenz
Copy link
Contributor

Suggesting to add a few simple "unit tests", because it should be easy to test and helps us verifying it works.

@Veratil
Copy link
Contributor Author

Veratil commented Jul 2, 2022

Suggesting to add a few simple "unit tests", because it should be easy to test and helps us verifying it works.

Done.

Also changed the current Qt-based encode/decode namespace to lmms::gui::base64, and kept the new in lmms::base64. This way we can adjust things to the new functions as needed.

@PhysSong
Copy link
Member

PhysSong commented Jul 2, 2022

Also changed the current Qt-based encode/decode namespace to lmms::gui::base64

Why in lmms::gui? We're removing Qt from core, but the base64 methods has nothing to do with GUI.

Also for the unit tests, I suggest iterating over original-encoded pairs and check encoding/decoding in the loop body, if possible.

@sakertooth
Copy link
Contributor

sakertooth commented Jul 2, 2022

Also changed the current Qt-based encode/decode namespace to lmms::gui::base64, and kept the new in lmms::base64. This way we can adjust things to the new functions as needed.

Why not just just provide an overload for QString and put the Qt implementation there? No need for these widespread changes in my honest opinion.

Once QString has been removed in the Qt-removal PR, we can remove the Qt Base64 functions as well.

@Veratil
Copy link
Contributor Author

Veratil commented Jul 2, 2022

Why in lmms::gui? We're removing Qt from core, but the base64 methods has nothing to do with GUI.

Whenever I left them in the same namespace I was getting errors about invalid overloads or something.
EDIT: I'm not getting them now. I don't know what was wrong before.

Also for the unit tests, I suggest iterating over original-encoded pairs and check encoding/decoding in the loop body, if possible.

They're just tests, but okay. :)

@PhysSong PhysSong mentioned this pull request Jan 15, 2023
Comment on lines +89 to +90
auto b64char1 = [](std::string_view chunk) {
return static_cast<std::string::value_type>(chunk[0] >> 2);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Any reason for passing std::string_view by value?
  • Given that std::string is an alias of std::basic_string<char>, std::string::value_type is always char.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • My understanding of string_view was that this is the way to do it. If not then I can change it. 👍
  • My thought was in the instance for some reason std::string isn't based on char.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding of string_view was that this is the way to do it.

It turns out you're correct, indeed.

Comment on lines +122 to +126
case 3:
output[3] = map[b64char4(chunk)];
case 2:
output[2] = map[b64char3(chunk)];
default: /* no-op */;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be good to add [[fallthrough]]; when falling through. I prefer explicitly breaking on case 2 and default, but I'll leave it up to you.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was not aware of this, I'll add them.

@PhysSong PhysSong marked this pull request as ready for review January 18, 2023 01:11

#endif
namespace lmms::base64 {
constexpr inline std::array<char, 64> map =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

constexpr implies inline, so inline should be redundant here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That might not be true for global variables, but regardless, I think all of these implementation details do not belong in the header anyway and should be moved to base64.cpp. Then they certainly will not need the inline.

'0','1','2','3','4','5','6','7','8','9',
'-','_'
};
const inline std::map<char, int> rmap {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of things:

  • Why not use constexpr here as well? If anything, it will be consistent with map.
  • Couldn't we have used a better name, maybe reverseMap instead of rmap?
  • Subjective, but could we also add a space in between the preceding and succeeding constexpr variables? (Usually I wouldn't mind if they were each one line, but they span multiple)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, std::map does not have a constexpr constructor so rmap cannot be constexpr, but I agree with your other points.

Comment on lines +87 to +107
/*
This section of math ensures that base64 encode/decode will work
as intended. Some rare architectures don't use 8-bit char's, and
it's possible this won't work as intended if a char isn't 8-bits.
In the rare case this is ported to an architecture where this
happens, feel free to comment out the static_assert's and test.
*/
constexpr int char_bits = std::numeric_limits<std::string_view::value_type>::digits;
constexpr int sign_bit = std::numeric_limits<std::string_view::value_type>::is_signed ? 1 : 0;
// check that the string_view character type is 8 (7 signed + 1 sign) bits wide
static_assert(char_bits + sign_bit == 8);
constexpr int numBitsPerChar = char_bits + sign_bit;
constexpr int numBitsPerBase64Char = 6;
constexpr int lcm = std::lcm(numBitsPerChar, numBitsPerBase64Char);
// make sure math works, 24 bits
static_assert(lcm == 24);
constexpr int numBytesPerChunk = lcm / numBitsPerChar;
constexpr int numBase64CharPerChunk = lcm / numBitsPerBase64Char;
// double check math works and bit width matches
static_assert(numBytesPerChunk * numBitsPerChar == numBase64CharPerChunk * numBitsPerBase64Char);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do like your attention towards portability, but a char not being 8 bits is very unlikely, especially given the architectures we are targeting. Even on architectures we aren't directly supporting (think ARM), char is still 8 bits.

This can be somewhat subjective, but the tradeoff for more code complexity over certain things that we can almost agree will certainly be the case, like a char being 8 bits, might not be worth it.

);
};
for (int currentChunk = 0; currentChunk < numChunks.quot; ++currentChunk) {
std::string_view chunk = data.substr(currentChunk * numBase64CharPerChunk, numBase64CharPerChunk);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think using auto here is a plus.

Suggested change
std::string_view chunk = data.substr(currentChunk * numBase64CharPerChunk, numBase64CharPerChunk);
auto chunk = data.substr(currentChunk * numBase64CharPerChunk, numBase64CharPerChunk);

Comment on lines +33 to +70
void create_test_data()
{
QTest::addColumn<QString>("original");
QTest::addColumn<QString>("encoded");

// Test Vectors from RFC 4648 Section 10
QTest::newRow("empty string") << "" << "";
QTest::newRow("1 chunk 2 pad") << "f" << "Zg==";
QTest::newRow("1 chunk 1 pad") << "fo" << "Zm8=";
QTest::newRow("1 chunk 0 pad") << "foo" << "Zm9v";
QTest::newRow("2 chunk 2 pad") << "foob" << "Zm9vYg==";
QTest::newRow("2 chunk 1 pad") << "fooba" << "Zm9vYmE=";
QTest::newRow("2 chunk 0 pad") << "foobar" << "Zm9vYmFy";
}
void b64_encode_data()
{
create_test_data();
}
void b64_encode()
{
using namespace lmms::base64;

QFETCH(QString, original);
QFETCH(QString, encoded);
QCOMPARE(QString(encode(original.toStdString()).c_str()), encoded);
}
void b64_decode_data()
{
create_test_data();
}
void b64_decode()
{
using namespace lmms::base64;

QFETCH(QString, original);
QFETCH(QString, encoded);
QCOMPARE(original, QString(decode(encoded.toStdString()).c_str()));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would personally add spaces between function definitions.

{
create_test_data();
}
void b64_decode()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
void b64_decode()
void b64Decode()

}
void b64_decode_data()
{
create_test_data();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
create_test_data();
createTestData();

}
void b64_encode_data()
{
create_test_data();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
create_test_data();
createTestData();

$<TARGET_OBJECTS:lmmsobjs>

src/core/AutomatableModelTest.cpp
src/core/base64Test.cpp
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be src/core/Base64Test.cpp.

@@ -0,0 +1,73 @@
/*
* base64Test.cpp
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* base64Test.cpp
* Base64Test.cpp

@sakertooth
Copy link
Contributor

Hey @Veratil, I would like to fix the merge conflicts here and try implementing this again if you would allow it. Don't want #7095 to just be closed (its their first PR, so I would like to merge it at some point), so I'll leave the unit test for that PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants