Skip to content

Commit cf10c93

Browse files
authored
Merge pull request #12044 from nanaya/parse-multiple-quoted
Fix parsing multiple quoted search options
2 parents 261f727 + f9d1c9c commit cf10c93

File tree

5 files changed

+16
-7
lines changed

5 files changed

+16
-7
lines changed

app/Libraries/Search/BeatmapsetQueryParser.php

+3-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ public static function parse(?string $query): array
1515
$options = [];
1616

1717
// reference: https://github.com/ppy/osu/blob/f6baf49ad6b42c662a729ad05e18bd99bc48b4c7/osu.Game/Screens/Select/FilterQueryParser.cs
18-
$keywords = preg_replace_callback('#\b(?<key>\w+)(?<op>(:|=|(>|<)(:|=)?))(?<value>(".*")|(\S*))#i', function ($m) use (&$options) {
18+
// adjusted for multiple quoted options (with side effect of inner quotes must be escaped)
19+
$keywords = preg_replace_callback('#\b(?<key>\w+)(?<op>(:|=|(>|<)(:|=)?))(?<value>("{1,2})(?:\\\"|.)*?\7|\S*)#i', function ($m) use (&$options) {
1920
$key = strtolower($m['key']);
2021
$op = str_replace(':', '=', $m['op']);
2122
switch ($key) {
@@ -240,7 +241,7 @@ private static function makeIntRangeOption($operator, $value)
240241
private static function makeTextOption(string $operator, string $value): ?string
241242
{
242243
return $operator === '='
243-
? presence(preg_replace('/^"(.*)"$/', '$1', $value))
244+
? presence(strtr(preg_replace('/^"(.*)"$/', '$1', $value), ['\\"' => '"']))
244245
: null;
245246
}
246247

resources/js/beatmapsets-show/header.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { observer } from 'mobx-react';
1414
import core from 'osu-core-singleton';
1515
import * as React from 'react';
1616
import { hasGuestOwners } from 'utils/beatmap-helper';
17-
import { downloadLimited, getArtist, getTitle, toggleFavourite } from 'utils/beatmapset-helper';
17+
import { downloadLimited, getArtist, getTitle, makeSearchQueryOption, toggleFavourite } from 'utils/beatmapset-helper';
1818
import { classWithModifiers } from 'utils/css';
1919
import { formatNumber } from 'utils/html';
2020
import { trans } from 'utils/lang';
@@ -146,7 +146,7 @@ export default class Header extends React.Component<Props> {
146146
<span className='beatmapset-header__details-text beatmapset-header__details-text--title'>
147147
<a
148148
className='beatmapset-header__details-text-link'
149-
href={route('beatmapsets.index', { q: `title=""${getTitle(this.controller.beatmapset)}""` })}
149+
href={route('beatmapsets.index', { q: makeSearchQueryOption('title', getTitle(this.controller.beatmapset)) })}
150150
>
151151
{getTitle(this.controller.beatmapset)}
152152
</a>
@@ -163,7 +163,7 @@ export default class Header extends React.Component<Props> {
163163
<span className='beatmapset-header__details-text beatmapset-header__details-text--artist'>
164164
<a
165165
className='beatmapset-header__details-text-link'
166-
href={route('beatmapsets.index', { q: `artist=""${getArtist(this.controller.beatmapset)}""` })}
166+
href={route('beatmapsets.index', { q: makeSearchQueryOption('artist', getArtist(this.controller.beatmapset)) })}
167167
>
168168
{getArtist(this.controller.beatmapset)}
169169
</a>

resources/js/beatmapsets-show/info.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { action, computed, makeObservable, observable, runInAction } from 'mobx'
1313
import { observer } from 'mobx-react';
1414
import * as React from 'react';
1515
import { onErrorWithClick } from 'utils/ajax';
16+
import { makeSearchQueryOption } from 'utils/beatmapset-helper';
1617
import { formatNumber } from 'utils/html';
1718
import { trans } from 'utils/lang';
1819
import { present } from 'utils/string';
@@ -154,7 +155,7 @@ export default class Info extends React.Component<Props> {
154155
</h3>
155156
<a
156157
className='beatmapset-info__link'
157-
href={route('beatmapsets.index', { q: `source=""${this.controller.beatmapset.source}""` })}
158+
href={route('beatmapsets.index', { q: makeSearchQueryOption('source', this.controller.beatmapset.source) })}
158159
>
159160
{this.controller.beatmapset.source}
160161
</a>

resources/js/utils/beatmapset-helper.ts

+4
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ export function getTitle(beatmapset: BeatmapsetJson) {
3434
return beatmapset.title;
3535
}
3636

37+
export function makeSearchQueryOption(key: string, value: string) {
38+
return `${key}=""${value.replace(/"/g, '\\"')}""`;
39+
}
40+
3741
export function showVisual(beatmapset: BeatmapsetJson) {
3842
return !beatmapset.nsfw || core.userPreferences.get('beatmapset_show_nsfw');
3943
}

tests/Libraries/Search/BeatmapsetQueryParserTest.php

+4-1
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,13 @@ public static function queryDataProvider()
4545
['ranked>2018/05/01', ['keywords' => null, 'options' => ['ranked' => ['gte' => static::parseTime('2018-05-02')]]]],
4646
['ranked>="2020-07-21 12:30:30 +09:00"', ['keywords' => null, 'options' => ['ranked' => ['gte' => static::parseTime('2020-07-21 03:30:30')]]]],
4747
['ranked="2020-07-21 12:30:30 +09:00"', ['keywords' => null, 'options' => ['ranked' => ['gte' => static::parseTime('2020-07-21 03:30:30'), 'lt' => static::parseTime('2020-07-21 03:30:31')]]]],
48+
['ranked>="2020-07-21 12:30:30 +09:00" ranked<="2020-08-21 13:40:40 +09:00"', ['keywords' => null, 'options' => ['ranked' => ['gte' => static::parseTime('2020-07-21 03:30:30'), 'lt' => static::parseTime('2020-08-21 04:40:41')]]]],
4849
['ranked="invalid date format"', ['keywords' => 'ranked="invalid date format"', 'options' => []]],
4950
['tag=hello', ['keywords' => null, 'options' => ['tag' => ['hello']]]],
5051
['tag=hello tag=world', ['keywords' => null, 'options' => ['tag' => ['hello', 'world']]]],
5152
['tag="hello world"', ['keywords' => null, 'options' => ['tag' => ['hello world']]]],
53+
['tag="hello world" tag="foo bar"', ['keywords' => null, 'options' => ['tag' => ['hello world', 'foo bar']]]],
54+
['tag="hello world"aa tag="foo bar"', ['keywords' => 'aa', 'options' => ['tag' => ['hello world', 'foo bar']]]],
5255

5356
// multiple options
5457
['artist=hello creator:world', ['keywords' => null, 'options' => ['artist' => 'hello', 'creator' => 'world']]],
@@ -88,7 +91,7 @@ public static function queryDataProvider()
8891
['find me songs by artist=singer please', ['keywords' => 'find me songs by please', 'options' => ['artist' => 'singer']]],
8992
['really like artist="name with space" yes', ['keywords' => 'really like yes', 'options' => ['artist' => 'name with space']]],
9093
['weird artist=double"quote', ['keywords' => 'weird', 'options' => ['artist' => 'double"quote']]],
91-
['weird artist="nested "quote"" thing', ['keywords' => 'weird thing', 'options' => ['artist' => 'nested "quote"']]],
94+
['weird artist="nested \"quote\"" thing', ['keywords' => 'weird thing', 'options' => ['artist' => 'nested "quote"']]],
9295
['artist=><something', ['keywords' => null, 'options' => ['artist' => '><something']]],
9396
['unrecognised=keyword', ['keywords' => 'unrecognised=keyword', 'options' => []]],
9497
['cs=nope', ['keywords' => 'cs=nope', 'options' => []]],

0 commit comments

Comments
 (0)