Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.25)

project(UltraLightBrowser
VERSION 2.4.0
VERSION 2.4.1
DESCRIPTION "Minimal, Ultra-Fast Windows 11 Native Browser Shell (macOS Safari UI Edition)"
LANGUAGES CXX C
)
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ tests/ 单元测试、页面资源检查、Windows 界面

## 最近更新

- **v2.4.1**:修复书签 HTML 导入:正确解析属性空格、制表符和完整属性名,避免把 DATA-HREF 或标题中的文字误当网址;按标准标记识别不同语言的收藏栏;超长网址不再虚报导入成功。新增覆盖以上情况的回归测试。
- **v2.4.0**:新增「显卡视频增强」(设置 → 高级,默认开):让 NVIDIA RTX 显卡做视频超分辨率和 SDR 转 HDR、Intel 显卡做超分;需在 NVIDIA App 里开启,接电源、画面被放大时生效。「播放诊断」显示视频放大倍数。
- **v2.3.0**:音频增强的最后一级换成真峰值限幅器:采样点和采样点之间的峰值都压在 -1 dBTP 以下,音量放大到 300% 也不会削波破音;约增加 4ms 延迟。少数安全策略严格、不允许加载的网站自动改用原来的压缩保护,「播放诊断」会显示用的是哪一种。
- **v2.2.3**:我的线路未连接时显示原因(本机没有 IPv6 / 连不到入口服务器 / 链接失效),便于排查。
Expand Down
8 changes: 4 additions & 4 deletions resources/resource.rc
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ IDI_APP_ICON ICON "app.ico"

// Version information
VS_VERSION_INFO VERSIONINFO
FILEVERSION 2,4,0,0
PRODUCTVERSION 2,4,0,0
FILEVERSION 2,4,1,0
PRODUCTVERSION 2,4,1,0
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
FILEFLAGS 0
FILEOS VOS_NT_WINDOWS32
Expand All @@ -48,12 +48,12 @@ BEGIN
BEGIN
VALUE "CompanyName", "UltraLightBrowser"
VALUE "FileDescription", "UltraLight Browser - Minimal, Ultra-Fast Windows 11 Native Browser (Safari-style UI)"
VALUE "FileVersion", "2.4.0.0"
VALUE "FileVersion", "2.4.1.0"
VALUE "InternalName", "UltraLightBrowser"
VALUE "LegalCopyright", "Copyright (C) 2026 Freecode100Year"
VALUE "OriginalFilename", "UltraLightBrowser.exe"
VALUE "ProductName", "UltraLight Browser"
VALUE "ProductVersion", "2.4.0.0"
VALUE "ProductVersion", "2.4.1.0"
END
END
BLOCK "VarFileInfo"
Expand Down
62 changes: 46 additions & 16 deletions src/Library.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -133,18 +133,31 @@ std::size_t FindCI(const std::string& hay, const std::string& needle, std::size_
}

std::string AttrValue(const std::string& tag, const std::string& name) {
auto pos = FindCI(tag, name + "=", 0);
if (pos == std::string::npos) return {};
pos += name.size() + 1;
if (pos >= tag.size()) return {};
const char quote = tag[pos];
if (quote == '"' || quote == '\'') {
const auto end = tag.find(quote, pos + 1);
if (end == std::string::npos) return {};
return tag.substr(pos + 1, end - pos - 1);
constexpr const char* whitespace = " \t\r\n\f";
// Walk complete attributes, skipping quoted values so DATA-HREF and text
// such as TITLE="href=..." cannot be mistaken for the actual link.
auto pos = tag.find_first_of(whitespace);
while (pos != std::string::npos && pos < tag.size()) {
pos = tag.find_first_not_of(whitespace, pos);
if (pos == std::string::npos) break;
const auto start = pos;
pos = tag.find_first_of("= \t\r\n\f", pos);
if (pos == std::string::npos) break;
const auto attribute = Lower(tag.substr(start, pos - start));
pos = tag.find_first_not_of(whitespace, pos);
if (pos == std::string::npos) break;
if (tag[pos] != '=') continue; // Boolean attribute; parse the next one.
pos = tag.find_first_not_of(whitespace, pos + 1);
if (pos == std::string::npos) break;
const char quote = tag[pos];
const bool quoted = quote == '"' || quote == '\'';
const auto valueStart = pos + (quoted ? 1 : 0);
const auto end = quoted ? tag.find(quote, valueStart) : tag.find_first_of(whitespace, valueStart);
if (quoted && end == std::string::npos) break;
if (attribute == name) return tag.substr(valueStart, end == std::string::npos ? end : end - valueStart);
pos = end == std::string::npos ? end : end + (quoted ? 1 : 0);
}
const auto end = tag.find_first_of(" >", pos);
return tag.substr(pos, end == std::string::npos ? std::string::npos : end - pos);
return {};
}

} // namespace
Expand Down Expand Up @@ -394,15 +407,33 @@ int Library::ImportNetscapeHtml(const std::string& html, std::int64_t now) {
while (pos < html.size()) {
const auto lt = html.find('<', pos);
if (lt == std::string::npos) break;
const auto gt = html.find('>', lt);
if (gt == std::string::npos) break;
if (html.compare(lt, 4, "<!--") == 0) {
const auto end = html.find("-->", lt + 4);
if (end == std::string::npos) break;
pos = end + 3;
continue;
}
auto gt = lt + 1;
char quote = 0;
for (; gt < html.size(); ++gt) {
const char c = html[gt];
if (quote) {
if (c == quote) quote = 0;
} else if (c == '"' || c == '\'') {
quote = c;
} else if (c == '>') {
break;
}
}
if (gt == html.size()) break;
std::string tag = html.substr(lt + 1, gt - lt - 1);
std::string name = Lower(tag.substr(0, tag.find_first_of(" \t\r\n")));
pos = gt + 1;
if (name == "h3") {
const auto close = FindCI(html, "</h3>", pos);
if (close == std::string::npos) break;
pendingFolder = Clip(Trim(HtmlUnescape(html.substr(pos, close - pos))), 128);
if (Lower(AttrValue(tag, "personal_toolbar_folder")) == "true") pendingFolder = kFavoritesFolder;
pos = close + 5;
} else if (name == "dl") {
folderStack.push_back(pendingFolder);
Expand All @@ -415,7 +446,7 @@ int Library::ImportNetscapeHtml(const std::string& html, std::int64_t now) {
const std::string url = HtmlUnescape(AttrValue(tag, "href"));
const std::string title = Trim(HtmlUnescape(html.substr(pos, close - pos)));
pos = close + 4;
if (!IsWebUrl(url)) continue;
if (!IsWebUrl(url) || url.size() > kMaxUrl) continue;
std::string folder = kBookmarksFolder;
for (auto it = folderStack.rbegin(); it != folderStack.rend(); ++it) {
if (!it->empty()) { folder = *it; break; }
Expand All @@ -427,8 +458,7 @@ int Library::ImportNetscapeHtml(const std::string& html, std::int64_t now) {
}
AddFolder(folder);
if (!existing.insert(folder + "\n" + url).second) continue;
AddBookmark(title, url, folder, now);
++imported;
if (!AddBookmark(title, url, folder, now).empty()) ++imported;
}
}
return imported;
Expand Down
31 changes: 31 additions & 0 deletions tests/library_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,41 @@

using namespace UltraLight;

static void TestBookmarkImport(const std::filesystem::path& dir) {
struct Case { const char* name; std::string html; int count; std::string url; std::string folder; };
const Case cases[] = {
{"attribute whitespace", R"(<A HREF = 'https://space.example/'>Space</A>)", 1, "https://space.example/", kBookmarksFolder},
{"attribute boundary", R"(<A DATA-HREF="https://wrong.example/" HREF="https://right.example/">Right</A>)", 1, "https://right.example/", kBookmarksFolder},
{"quoted attribute content", R"(<A TITLE="href=https://wrong.example/" HREF="https://right.example/">Right</A>)", 1, "https://right.example/", kBookmarksFolder},
{"quoted tag delimiter", R"(<A TITLE="a > b" HREF="https://right.example/">Right</A>)", 1, "https://right.example/", kBookmarksFolder},
{"comment text is not markup", R"(<!-- User's <A HREF="https://wrong.example/">old link</A> --><A HREF="https://right.example/">Right</A>)", 1, "https://right.example/", kBookmarksFolder},
{"tab-delimited unquoted URL", "<A HREF=https://tab.example/\tADD_DATE=1>Tab</A>", 1, "https://tab.example/", kBookmarksFolder},
{"boolean and mixed-case attributes", R"(<A DISABLED HrEf="https://case.example/">Case</A>)", 1, "https://case.example/", kBookmarksFolder},
{"data attribute alone is not a link", R"(<A DATA-HREF="https://wrong.example/">Not a link</A>)", 0, "", kBookmarksFolder},
{"unterminated attribute rejected", R"(<A HREF="https://bad.example/>Bad</A>)", 0, "", kBookmarksFolder},
{"localized toolbar", R"(<H3 PERSONAL_TOOLBAR_FOLDER="true">Meine Favoriten</H3><DL><A HREF="https://bar.example/">Bar</A></DL>)", 1, "https://bar.example/", kFavoritesFolder},
{"non-toolbar folder preserved", R"(<H3 PERSONAL_TOOLBAR_FOLDER="false">Meine Favoriten</H3><DL><A HREF="https://folder.example/">Folder</A></DL>)", 1, "https://folder.example/", "Meine Favoriten"},
{"URL size boundary accepted", "<A HREF=\"https://large.example/" + std::string(8170, 'a') + "\">Large</A>", 1, "https://large.example/" + std::string(8170, 'a'), kBookmarksFolder},
{"oversized URL rejected", "<A HREF=\"https://large.example/" + std::string(8192, 'a') + "\">Large</A>", 0, "", kBookmarksFolder},
};
int failures = 0;
for (const auto& c : cases) {
Library lib(dir / "import-cases");
const int count = lib.ImportNetscapeHtml(c.html, 1);
if (count != c.count || lib.Bookmarks().size() != static_cast<std::size_t>(c.count) ||
(c.count && (lib.Bookmarks().front().url != c.url || lib.Bookmarks().front().folder != c.folder))) {
std::cerr << "FAIL: bookmark import " << c.name << '\n';
++failures;
}
}
assert(failures == 0);
}

int main() {
const auto dir = std::filesystem::temp_directory_path() / "ulb-library-test";
std::filesystem::remove_all(dir);
std::filesystem::create_directories(dir);
TestBookmarkImport(dir);
std::ofstream(dir / "history.json") << R"([{"url":"https://old.example/","title":"x","last":1,"visits":1}])";
std::ofstream(dir / "session.json") << "[]";
{
Expand Down
Loading