Skip to content

Commit 9f59675

Browse files
committed
Hash the URI as part of the hyperlink ID (#7940)
It turns out that we missed part of the OSC 8 spec which indicated that _hyperlinks with the same ID but different URIs are logically distinct._ > Character cells that have the same target URI and the same nonempty id > are always underlined together on mouseover. > The same id is only used for connecting character cells whose URIs is > also the same. Character cells pointing to different URIs should never > be underlined together when hovering over. This pull request fixes that oversight by appending the (hashed) URI to the generated ID. When Terminal receives one of these links over ConPTY, it will hash the URL a second time and therefore append a second hashed ID. This is taken as an acceptable cost. Fixes #7698 (cherry picked from commit df7c3cc)
1 parent 72bedcc commit 9f59675

File tree

7 files changed

+87
-20
lines changed

7 files changed

+87
-20
lines changed

src/buffer/out/textBuffer.cpp

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2281,32 +2281,35 @@ std::wstring TextBuffer::GetHyperlinkUriFromId(uint16_t id) const
22812281
// - The user-defined id
22822282
// Return value:
22832283
// - The internal hyperlink ID
2284-
uint16_t TextBuffer::GetHyperlinkId(std::wstring_view params)
2284+
uint16_t TextBuffer::GetHyperlinkId(std::wstring_view uri, std::wstring_view id)
22852285
{
2286-
uint16_t id = 0;
2287-
if (params.empty())
2286+
uint16_t numericId = 0;
2287+
if (id.empty())
22882288
{
22892289
// no custom id specified, return our internal count
2290-
id = _currentHyperlinkId;
2290+
numericId = _currentHyperlinkId;
22912291
++_currentHyperlinkId;
22922292
}
22932293
else
22942294
{
22952295
// assign _currentHyperlinkId if the custom id does not already exist
2296-
const auto result = _hyperlinkCustomIdMap.emplace(params, _currentHyperlinkId);
2296+
std::wstring newId{ id };
2297+
// hash the URL and add it to the custom ID - GH#7698
2298+
newId += L"%" + std::to_wstring(std::hash<std::wstring_view>{}(uri));
2299+
const auto result = _hyperlinkCustomIdMap.emplace(newId, _currentHyperlinkId);
22972300
if (result.second)
22982301
{
22992302
// the custom id did not already exist
23002303
++_currentHyperlinkId;
23012304
}
2302-
id = (*(result.first)).second;
2305+
numericId = (*(result.first)).second;
23032306
}
23042307
// _currentHyperlinkId could overflow, make sure its not 0
23052308
if (_currentHyperlinkId == 0)
23062309
{
23072310
++_currentHyperlinkId;
23082311
}
2309-
return id;
2312+
return numericId;
23102313
}
23112314

23122315
// Method Description:

src/buffer/out/textBuffer.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ class TextBuffer final
143143

144144
void AddHyperlinkToMap(std::wstring_view uri, uint16_t id);
145145
std::wstring GetHyperlinkUriFromId(uint16_t id) const;
146-
uint16_t GetHyperlinkId(std::wstring_view params);
146+
uint16_t GetHyperlinkId(std::wstring_view uri, std::wstring_view id);
147147
void RemoveHyperlinkFromMap(uint16_t id);
148148
std::wstring GetCustomIdFromId(uint16_t id) const;
149149
void CopyHyperlinkMaps(const TextBuffer& OtherBuffer);

src/cascadia/TerminalCore/TerminalApi.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,7 @@ CATCH_LOG_RETURN_FALSE()
574574
bool Terminal::AddHyperlink(std::wstring_view uri, std::wstring_view params) noexcept
575575
{
576576
auto attr = _buffer->GetCurrentAttributes();
577-
const auto id = _buffer->GetHyperlinkId(params);
577+
const auto id = _buffer->GetHyperlinkId(uri, params);
578578
attr.SetHyperlinkId(id);
579579
_buffer->SetCurrentAttributes(attr);
580580
_buffer->AddHyperlinkToMap(uri, id);

src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ namespace TerminalCoreUnitTests
3737

3838
TEST_METHOD(AddHyperlink);
3939
TEST_METHOD(AddHyperlinkCustomId);
40+
TEST_METHOD(AddHyperlinkCustomIdDifferentUri);
4041
};
4142
};
4243

@@ -296,15 +297,45 @@ void TerminalCoreUnitTests::TerminalApiTest::AddHyperlinkCustomId()
296297
stateMachine.ProcessString(L"\x1b]8;id=myId;test.url\x9c");
297298
VERIFY_IS_TRUE(tbi.GetCurrentAttributes().IsHyperlink());
298299
VERIFY_ARE_EQUAL(tbi.GetHyperlinkUriFromId(tbi.GetCurrentAttributes().GetHyperlinkId()), L"test.url");
299-
VERIFY_ARE_EQUAL(tbi.GetHyperlinkId(L"myId"), tbi.GetCurrentAttributes().GetHyperlinkId());
300+
VERIFY_ARE_EQUAL(tbi.GetHyperlinkId(L"test.url", L"myId"), tbi.GetCurrentAttributes().GetHyperlinkId());
300301

301302
// Send any other text
302303
stateMachine.ProcessString(L"Hello World");
303304
VERIFY_IS_TRUE(tbi.GetCurrentAttributes().IsHyperlink());
304305
VERIFY_ARE_EQUAL(tbi.GetHyperlinkUriFromId(tbi.GetCurrentAttributes().GetHyperlinkId()), L"test.url");
305-
VERIFY_ARE_EQUAL(tbi.GetHyperlinkId(L"myId"), tbi.GetCurrentAttributes().GetHyperlinkId());
306+
VERIFY_ARE_EQUAL(tbi.GetHyperlinkId(L"test.url", L"myId"), tbi.GetCurrentAttributes().GetHyperlinkId());
306307

307308
// Process the closing osc 8 sequences
308309
stateMachine.ProcessString(L"\x1b]8;;\x9c");
309310
VERIFY_IS_FALSE(tbi.GetCurrentAttributes().IsHyperlink());
310311
}
312+
313+
void TerminalCoreUnitTests::TerminalApiTest::AddHyperlinkCustomIdDifferentUri()
314+
{
315+
// This is a nearly literal copy-paste of ScreenBufferTests::TestAddHyperlinkCustomId, adapted for the Terminal
316+
317+
Terminal term;
318+
DummyRenderTarget emptyRT;
319+
term.Create({ 100, 100 }, 0, emptyRT);
320+
321+
auto& tbi = *(term._buffer);
322+
auto& stateMachine = *(term._stateMachine);
323+
324+
// Process the opening osc 8 sequence
325+
stateMachine.ProcessString(L"\x1b]8;id=myId;test.url\x9c");
326+
VERIFY_IS_TRUE(tbi.GetCurrentAttributes().IsHyperlink());
327+
VERIFY_ARE_EQUAL(tbi.GetHyperlinkUriFromId(tbi.GetCurrentAttributes().GetHyperlinkId()), L"test.url");
328+
VERIFY_ARE_EQUAL(tbi.GetHyperlinkId(L"test.url", L"myId"), tbi.GetCurrentAttributes().GetHyperlinkId());
329+
330+
const auto oldAttributes{ tbi.GetCurrentAttributes() };
331+
332+
// Send any other text
333+
stateMachine.ProcessString(L"\x1b]8;id=myId;other.url\x9c");
334+
VERIFY_IS_TRUE(tbi.GetCurrentAttributes().IsHyperlink());
335+
VERIFY_ARE_EQUAL(tbi.GetHyperlinkUriFromId(tbi.GetCurrentAttributes().GetHyperlinkId()), L"other.url");
336+
VERIFY_ARE_EQUAL(tbi.GetHyperlinkId(L"other.url", L"myId"), tbi.GetCurrentAttributes().GetHyperlinkId());
337+
338+
// This second URL should not change the URL of the original ID!
339+
VERIFY_ARE_EQUAL(tbi.GetHyperlinkUriFromId(oldAttributes.GetHyperlinkId()), L"test.url");
340+
VERIFY_ARE_NOT_EQUAL(oldAttributes.GetHyperlinkId(), tbi.GetCurrentAttributes().GetHyperlinkId());
341+
}

src/host/getset.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1567,7 +1567,7 @@ void DoSrvAddHyperlink(SCREEN_INFORMATION& screenInfo,
15671567
const std::wstring_view params)
15681568
{
15691569
auto attr = screenInfo.GetAttributes();
1570-
const auto id = screenInfo.GetTextBuffer().GetHyperlinkId(params);
1570+
const auto id = screenInfo.GetTextBuffer().GetHyperlinkId(uri, params);
15711571
attr.SetHyperlinkId(id);
15721572
screenInfo.GetTextBuffer().SetCurrentAttributes(attr);
15731573
screenInfo.GetTextBuffer().AddHyperlinkToMap(uri, id);

src/host/ut_host/ScreenBufferTests.cpp

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ class ScreenBufferTests
213213

214214
TEST_METHOD(TestAddHyperlink);
215215
TEST_METHOD(TestAddHyperlinkCustomId);
216+
TEST_METHOD(TestAddHyperlinkCustomIdDifferentUri);
216217

217218
TEST_METHOD(UpdateVirtualBottomWhenCursorMovesBelowIt);
218219

@@ -5956,19 +5957,46 @@ void ScreenBufferTests::TestAddHyperlinkCustomId()
59565957
stateMachine.ProcessString(L"\x1b]8;id=myId;test.url\x9c");
59575958
VERIFY_IS_TRUE(tbi.GetCurrentAttributes().IsHyperlink());
59585959
VERIFY_ARE_EQUAL(tbi.GetHyperlinkUriFromId(tbi.GetCurrentAttributes().GetHyperlinkId()), L"test.url");
5959-
VERIFY_ARE_EQUAL(tbi.GetHyperlinkId(L"myId"), tbi.GetCurrentAttributes().GetHyperlinkId());
5960+
VERIFY_ARE_EQUAL(tbi.GetHyperlinkId(L"test.url", L"myId"), tbi.GetCurrentAttributes().GetHyperlinkId());
59605961

59615962
// Send any other text
59625963
stateMachine.ProcessString(L"Hello World");
59635964
VERIFY_IS_TRUE(tbi.GetCurrentAttributes().IsHyperlink());
59645965
VERIFY_ARE_EQUAL(tbi.GetHyperlinkUriFromId(tbi.GetCurrentAttributes().GetHyperlinkId()), L"test.url");
5965-
VERIFY_ARE_EQUAL(tbi.GetHyperlinkId(L"myId"), tbi.GetCurrentAttributes().GetHyperlinkId());
5966+
VERIFY_ARE_EQUAL(tbi.GetHyperlinkId(L"test.url", L"myId"), tbi.GetCurrentAttributes().GetHyperlinkId());
59665967

59675968
// Process the closing osc 8 sequences
59685969
stateMachine.ProcessString(L"\x1b]8;;\x9c");
59695970
VERIFY_IS_FALSE(tbi.GetCurrentAttributes().IsHyperlink());
59705971
}
59715972

5973+
void ScreenBufferTests::TestAddHyperlinkCustomIdDifferentUri()
5974+
{
5975+
auto& g = ServiceLocator::LocateGlobals();
5976+
auto& gci = g.getConsoleInformation();
5977+
auto& si = gci.GetActiveOutputBuffer();
5978+
auto& tbi = si.GetTextBuffer();
5979+
auto& stateMachine = si.GetStateMachine();
5980+
5981+
// Process the opening osc 8 sequence with a custom id
5982+
stateMachine.ProcessString(L"\x1b]8;id=myId;test.url\x9c");
5983+
VERIFY_IS_TRUE(tbi.GetCurrentAttributes().IsHyperlink());
5984+
VERIFY_ARE_EQUAL(tbi.GetHyperlinkUriFromId(tbi.GetCurrentAttributes().GetHyperlinkId()), L"test.url");
5985+
VERIFY_ARE_EQUAL(tbi.GetHyperlinkId(L"test.url", L"myId"), tbi.GetCurrentAttributes().GetHyperlinkId());
5986+
5987+
const auto oldAttributes{ tbi.GetCurrentAttributes() };
5988+
5989+
// Send any other text
5990+
stateMachine.ProcessString(L"\x1b]8;id=myId;other.url\x9c");
5991+
VERIFY_IS_TRUE(tbi.GetCurrentAttributes().IsHyperlink());
5992+
VERIFY_ARE_EQUAL(tbi.GetHyperlinkUriFromId(tbi.GetCurrentAttributes().GetHyperlinkId()), L"other.url");
5993+
VERIFY_ARE_EQUAL(tbi.GetHyperlinkId(L"other.url", L"myId"), tbi.GetCurrentAttributes().GetHyperlinkId());
5994+
5995+
// This second URL should not change the URL of the original ID!
5996+
VERIFY_ARE_EQUAL(tbi.GetHyperlinkUriFromId(oldAttributes.GetHyperlinkId()), L"test.url");
5997+
VERIFY_ARE_NOT_EQUAL(oldAttributes.GetHyperlinkId(), tbi.GetCurrentAttributes().GetHyperlinkId());
5998+
}
5999+
59726000
void ScreenBufferTests::UpdateVirtualBottomWhenCursorMovesBelowIt()
59736001
{
59746002
auto& g = ServiceLocator::LocateGlobals();

src/host/ut_host/TextBufferTests.cpp

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2546,30 +2546,33 @@ void TextBufferTests::HyperlinkTrim()
25462546

25472547
// Set a hyperlink id in the first row and add a hyperlink to our map
25482548
const COORD pos{ 70, 0 };
2549-
const auto id = _buffer->GetHyperlinkId(customId);
2549+
const auto id = _buffer->GetHyperlinkId(url, customId);
25502550
TextAttribute newAttr{ 0x7f };
25512551
newAttr.SetHyperlinkId(id);
25522552
_buffer->GetRowByOffset(pos.Y).GetAttrRow().SetAttrToEnd(pos.X, newAttr);
25532553
_buffer->AddHyperlinkToMap(url, id);
25542554

25552555
// Set a different hyperlink id somewhere else in the buffer
25562556
const COORD otherPos{ 70, 5 };
2557-
const auto otherId = _buffer->GetHyperlinkId(otherCustomId);
2557+
const auto otherId = _buffer->GetHyperlinkId(otherUrl, otherCustomId);
25582558
newAttr.SetHyperlinkId(otherId);
25592559
_buffer->GetRowByOffset(otherPos.Y).GetAttrRow().SetAttrToEnd(otherPos.X, newAttr);
25602560
_buffer->AddHyperlinkToMap(otherUrl, otherId);
25612561

25622562
// Increment the circular buffer
25632563
_buffer->IncrementCircularBuffer();
25642564

2565+
const auto finalCustomId = fmt::format(L"{}%{}", customId, std::hash<std::wstring_view>{}(url));
2566+
const auto finalOtherCustomId = fmt::format(L"{}%{}", otherCustomId, std::hash<std::wstring_view>{}(otherUrl));
2567+
25652568
// The hyperlink reference that was only in the first row should be deleted from the map
25662569
VERIFY_ARE_EQUAL(_buffer->_hyperlinkMap.find(id), _buffer->_hyperlinkMap.end());
25672570
// Since there was a custom id, that should be deleted as well
2568-
VERIFY_ARE_EQUAL(_buffer->_hyperlinkCustomIdMap.find(customId), _buffer->_hyperlinkCustomIdMap.end());
2571+
VERIFY_ARE_EQUAL(_buffer->_hyperlinkCustomIdMap.find(finalCustomId), _buffer->_hyperlinkCustomIdMap.end());
25692572

25702573
// The other hyperlink reference should not be deleted
25712574
VERIFY_ARE_EQUAL(_buffer->_hyperlinkMap[otherId], otherUrl);
2572-
VERIFY_ARE_EQUAL(_buffer->_hyperlinkCustomIdMap[otherCustomId], otherId);
2575+
VERIFY_ARE_EQUAL(_buffer->_hyperlinkCustomIdMap[finalOtherCustomId], otherId);
25732576
}
25742577

25752578
// This tests that when we increment the circular buffer, non-obsolete hyperlink references
@@ -2587,7 +2590,7 @@ void TextBufferTests::NoHyperlinkTrim()
25872590

25882591
// Set a hyperlink id in the first row and add a hyperlink to our map
25892592
const COORD pos{ 70, 0 };
2590-
const auto id = _buffer->GetHyperlinkId(customId);
2593+
const auto id = _buffer->GetHyperlinkId(url, customId);
25912594
TextAttribute newAttr{ 0x7f };
25922595
newAttr.SetHyperlinkId(id);
25932596
_buffer->GetRowByOffset(pos.Y).GetAttrRow().SetAttrToEnd(pos.X, newAttr);
@@ -2600,7 +2603,9 @@ void TextBufferTests::NoHyperlinkTrim()
26002603
// Increment the circular buffer
26012604
_buffer->IncrementCircularBuffer();
26022605

2606+
const auto finalCustomId = fmt::format(L"{}%{}", customId, std::hash<std::wstring_view>{}(url));
2607+
26032608
// The hyperlink reference should not be deleted from the map since it is still present in the buffer
26042609
VERIFY_ARE_EQUAL(_buffer->GetHyperlinkUriFromId(id), url);
2605-
VERIFY_ARE_EQUAL(_buffer->_hyperlinkCustomIdMap[customId], id);
2610+
VERIFY_ARE_EQUAL(_buffer->_hyperlinkCustomIdMap[finalCustomId], id);
26062611
}

0 commit comments

Comments
 (0)