StringLibrary.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  1. using System.Text;
  2. using System.Text.RegularExpressions;
  3. using Lua.Internal;
  4. using Lua.Runtime;
  5. namespace Lua.Standard;
  6. public sealed class StringLibrary
  7. {
  8. public static readonly StringLibrary Instance = new();
  9. public StringLibrary()
  10. {
  11. Functions = [
  12. new("byte", Byte),
  13. new("char", Char),
  14. new("dump", Dump),
  15. new("find", Find),
  16. new("format", Format),
  17. new("gmatch", GMatch),
  18. new("gsub", GSub),
  19. new("len", Len),
  20. new("lower", Lower),
  21. new("rep", Rep),
  22. new("reverse", Reverse),
  23. new("sub", Sub),
  24. new("upper", Upper),
  25. ];
  26. }
  27. public readonly LuaFunction[] Functions;
  28. public ValueTask<int> Byte(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
  29. {
  30. var s = context.GetArgument<string>(0);
  31. var i = context.HasArgument(1)
  32. ? context.GetArgument<double>(1)
  33. : 1;
  34. var j = context.HasArgument(2)
  35. ? context.GetArgument<double>(2)
  36. : i;
  37. LuaRuntimeException.ThrowBadArgumentIfNumberIsNotInteger(context.State, "byte", 2, i);
  38. LuaRuntimeException.ThrowBadArgumentIfNumberIsNotInteger(context.State, "byte", 3, j);
  39. var span = StringHelper.Slice(s, (int)i, (int)j);
  40. for (int k = 0; k < span.Length; k++)
  41. {
  42. buffer.Span[k] = span[k];
  43. }
  44. return new(span.Length);
  45. }
  46. public ValueTask<int> Char(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
  47. {
  48. if (context.ArgumentCount == 0)
  49. {
  50. buffer.Span[0] = "";
  51. return new(1);
  52. }
  53. var builder = new ValueStringBuilder(context.ArgumentCount);
  54. for (int i = 0; i < context.ArgumentCount; i++)
  55. {
  56. var arg = context.GetArgument<double>(i);
  57. LuaRuntimeException.ThrowBadArgumentIfNumberIsNotInteger(context.State, "char", i + 1, arg);
  58. builder.Append((char)arg);
  59. }
  60. buffer.Span[0] = builder.ToString();
  61. return new(1);
  62. }
  63. public ValueTask<int> Dump(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
  64. {
  65. // stirng.dump is not supported (throw exception)
  66. throw new NotSupportedException("stirng.dump is not supported");
  67. }
  68. public ValueTask<int> Find(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
  69. {
  70. var s = context.GetArgument<string>(0);
  71. var pattern = context.GetArgument<string>(1);
  72. var init = context.HasArgument(2)
  73. ? context.GetArgument<double>(2)
  74. : 1;
  75. var plain = context.HasArgument(3)
  76. ? context.GetArgument(3).ToBoolean()
  77. : false;
  78. LuaRuntimeException.ThrowBadArgumentIfNumberIsNotInteger(context.State, "find", 3, init);
  79. // init can be negative value
  80. if (init < 0)
  81. {
  82. init = s.Length + init + 1;
  83. }
  84. // out of range
  85. if (init != 1 && (init < 1 || init > s.Length))
  86. {
  87. buffer.Span[0] = LuaValue.Nil;
  88. return new(1);
  89. }
  90. // empty pattern
  91. if (pattern.Length == 0)
  92. {
  93. buffer.Span[0] = 1;
  94. buffer.Span[1] = 1;
  95. return new(2);
  96. }
  97. var source = s.AsSpan()[(int)(init - 1)..];
  98. if (plain)
  99. {
  100. var start = source.IndexOf(pattern);
  101. if (start == -1)
  102. {
  103. buffer.Span[0] = LuaValue.Nil;
  104. return new(1);
  105. }
  106. // 1-based
  107. buffer.Span[0] = start + 1;
  108. buffer.Span[1] = start + pattern.Length;
  109. return new(2);
  110. }
  111. else
  112. {
  113. var regex = StringHelper.ToRegex(pattern);
  114. var match = regex.Match(source.ToString());
  115. if (match.Success)
  116. {
  117. // 1-based
  118. buffer.Span[0] = init + match.Index;
  119. buffer.Span[1] = init + match.Index + match.Length - 1;
  120. return new(2);
  121. }
  122. else
  123. {
  124. buffer.Span[0] = LuaValue.Nil;
  125. return new(1);
  126. }
  127. }
  128. }
  129. public async ValueTask<int> Format(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
  130. {
  131. var format = context.GetArgument<string>(0);
  132. // TODO: pooling StringBuilder
  133. var builder = new StringBuilder(format.Length * 2);
  134. var parameterIndex = 1;
  135. for (int i = 0; i < format.Length; i++)
  136. {
  137. if (format[i] == '%')
  138. {
  139. i++;
  140. // escape
  141. if (format[i] == '%')
  142. {
  143. builder.Append('%');
  144. continue;
  145. }
  146. var leftJustify = false;
  147. var plusSign = false;
  148. var zeroPadding = false;
  149. var alternateForm = false;
  150. var blank = false;
  151. var width = 0;
  152. var precision = -1;
  153. // Process flags
  154. while (true)
  155. {
  156. var c = format[i];
  157. switch (c)
  158. {
  159. case '-':
  160. if (leftJustify) throw new LuaRuntimeException(context.State.GetTraceback(), "invalid format (repeated flags)");
  161. leftJustify = true;
  162. break;
  163. case '+':
  164. if (plusSign) throw new LuaRuntimeException(context.State.GetTraceback(), "invalid format (repeated flags)");
  165. plusSign = true;
  166. break;
  167. case '0':
  168. if (zeroPadding) throw new LuaRuntimeException(context.State.GetTraceback(), "invalid format (repeated flags)");
  169. zeroPadding = true;
  170. break;
  171. case '#':
  172. if (alternateForm) throw new LuaRuntimeException(context.State.GetTraceback(), "invalid format (repeated flags)");
  173. alternateForm = true;
  174. break;
  175. case ' ':
  176. if (blank) throw new LuaRuntimeException(context.State.GetTraceback(), "invalid format (repeated flags)");
  177. blank = true;
  178. break;
  179. default:
  180. goto PROCESS_WIDTH;
  181. }
  182. i++;
  183. }
  184. PROCESS_WIDTH:
  185. // Process width
  186. var start = i;
  187. if (char.IsDigit(format[i]))
  188. {
  189. i++;
  190. if (char.IsDigit(format[i])) i++;
  191. if (char.IsDigit(format[i])) throw new LuaRuntimeException(context.State.GetTraceback(), "invalid format (width or precision too long)");
  192. width = int.Parse(format.AsSpan()[start..i]);
  193. }
  194. // Process precision
  195. if (format[i] == '.')
  196. {
  197. i++;
  198. start = i;
  199. if (char.IsDigit(format[i])) i++;
  200. if (char.IsDigit(format[i])) i++;
  201. if (char.IsDigit(format[i])) throw new LuaRuntimeException(context.State.GetTraceback(), "invalid format (width or precision too long)");
  202. precision = int.Parse(format.AsSpan()[start..i]);
  203. }
  204. // Process conversion specifier
  205. var specifier = format[i];
  206. if (context.ArgumentCount <= parameterIndex)
  207. {
  208. throw new LuaRuntimeException(context.State.GetTraceback(), $"bad argument #{parameterIndex + 1} to 'format' (no value)");
  209. }
  210. var parameter = context.GetArgument(parameterIndex++);
  211. // TODO: reduce allocation
  212. string formattedValue = default!;
  213. switch (specifier)
  214. {
  215. case 'f':
  216. case 'e':
  217. case 'g':
  218. case 'G':
  219. if (!parameter.TryRead<double>(out var f))
  220. {
  221. LuaRuntimeException.BadArgument(context.State.GetTraceback(), parameterIndex + 1, "format", LuaValueType.Number.ToString(), parameter.Type.ToString());
  222. }
  223. switch (specifier)
  224. {
  225. case 'f':
  226. formattedValue = precision < 0
  227. ? f.ToString()
  228. : f.ToString($"F{precision}");
  229. break;
  230. case 'e':
  231. formattedValue = precision < 0
  232. ? f.ToString()
  233. : f.ToString($"E{precision}");
  234. break;
  235. case 'g':
  236. formattedValue = precision < 0
  237. ? f.ToString()
  238. : f.ToString($"G{precision}");
  239. break;
  240. case 'G':
  241. formattedValue = precision < 0
  242. ? f.ToString().ToUpper()
  243. : f.ToString($"G{precision}").ToUpper();
  244. break;
  245. }
  246. if (plusSign && f >= 0)
  247. {
  248. formattedValue = $"+{formattedValue}";
  249. }
  250. break;
  251. case 's':
  252. using (var strBuffer = new PooledArray<LuaValue>(1))
  253. {
  254. await parameter.CallToStringAsync(context, strBuffer.AsMemory(), cancellationToken);
  255. formattedValue = strBuffer[0].Read<string>();
  256. }
  257. if (specifier is 's' && precision > 0 && precision <= formattedValue.Length)
  258. {
  259. formattedValue = formattedValue[..precision];
  260. }
  261. break;
  262. case 'q':
  263. switch (parameter.Type)
  264. {
  265. case LuaValueType.Nil:
  266. formattedValue = "nil";
  267. break;
  268. case LuaValueType.Boolean:
  269. formattedValue = parameter.Read<bool>() ? "true" : "false";
  270. break;
  271. case LuaValueType.String:
  272. formattedValue = $"\"{StringHelper.Escape(parameter.Read<string>())}\"";
  273. break;
  274. case LuaValueType.Number:
  275. // TODO: floating point numbers must be in hexadecimal notation
  276. formattedValue = parameter.Read<double>().ToString();
  277. break;
  278. default:
  279. using (var strBuffer = new PooledArray<LuaValue>(1))
  280. {
  281. await parameter.CallToStringAsync(context, strBuffer.AsMemory(), cancellationToken);
  282. formattedValue = strBuffer[0].Read<string>();
  283. }
  284. break;
  285. }
  286. break;
  287. case 'i':
  288. case 'd':
  289. case 'u':
  290. case 'c':
  291. case 'x':
  292. case 'X':
  293. if (!parameter.TryRead<double>(out var x))
  294. {
  295. LuaRuntimeException.BadArgument(context.State.GetTraceback(), parameterIndex + 1, "format", LuaValueType.Number.ToString(), parameter.Type.ToString());
  296. }
  297. LuaRuntimeException.ThrowBadArgumentIfNumberIsNotInteger(context.State, "format", parameterIndex + 1, x);
  298. switch (specifier)
  299. {
  300. case 'i':
  301. case 'd':
  302. {
  303. var integer = checked((long)x);
  304. formattedValue = precision < 0
  305. ? integer.ToString()
  306. : integer.ToString($"D{precision}");
  307. }
  308. break;
  309. case 'u':
  310. {
  311. var integer = checked((ulong)x);
  312. formattedValue = precision < 0
  313. ? integer.ToString()
  314. : integer.ToString($"D{precision}");
  315. }
  316. break;
  317. case 'c':
  318. formattedValue = ((char)(int)x).ToString();
  319. break;
  320. case 'x':
  321. {
  322. var integer = checked((ulong)x);
  323. formattedValue = alternateForm
  324. ? $"0x{integer:x}"
  325. : $"{integer:x}";
  326. }
  327. break;
  328. case 'X':
  329. {
  330. var integer = checked((ulong)x);
  331. formattedValue = alternateForm
  332. ? $"0X{integer:X}"
  333. : $"{integer:X}";
  334. }
  335. break;
  336. case 'o':
  337. {
  338. var integer = checked((long)x);
  339. formattedValue = Convert.ToString(integer, 8);
  340. }
  341. break;
  342. }
  343. if (plusSign && x >= 0)
  344. {
  345. formattedValue = $"+{formattedValue}";
  346. }
  347. break;
  348. default:
  349. throw new LuaRuntimeException(context.State.GetTraceback(), $"invalid option '%{specifier}' to 'format'");
  350. }
  351. // Apply blank (' ') flag for positive numbers
  352. if (specifier is 'd' or 'i' or 'f' or 'g' or 'G')
  353. {
  354. if (blank && !leftJustify && !zeroPadding && parameter.Read<double>() >= 0)
  355. {
  356. formattedValue = $" {formattedValue}";
  357. }
  358. }
  359. // Apply width and padding
  360. if (width > formattedValue.Length)
  361. {
  362. if (leftJustify)
  363. {
  364. formattedValue = formattedValue.PadRight(width);
  365. }
  366. else
  367. {
  368. formattedValue = zeroPadding ? formattedValue.PadLeft(width, '0') : formattedValue.PadLeft(width);
  369. }
  370. }
  371. builder.Append(formattedValue);
  372. }
  373. else
  374. {
  375. builder.Append(format[i]);
  376. }
  377. }
  378. buffer.Span[0] = builder.ToString();
  379. return 1;
  380. }
  381. public ValueTask<int> GMatch(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
  382. {
  383. var s = context.GetArgument<string>(0);
  384. var pattern = context.GetArgument<string>(1);
  385. var regex = StringHelper.ToRegex(pattern);
  386. var matches = regex.Matches(s);
  387. buffer.Span[0] = new CsClosure("iterator",[new LuaValue(matches),0],static (context, buffer, cancellationToken) =>
  388. {
  389. var upValues = context.GetCsClosure()!.UpValues;
  390. var matches = upValues[0].Read<MatchCollection>();
  391. var i = upValues[1].Read<int>();
  392. if (matches.Count > i)
  393. {
  394. var match = matches[i];
  395. var groups = match.Groups;
  396. i++;
  397. upValues[1] = i;
  398. if (groups.Count == 1)
  399. {
  400. buffer.Span[0] = match.Value;
  401. }
  402. else
  403. {
  404. for (int j = 0; j < groups.Count; j++)
  405. {
  406. buffer.Span[j] = groups[j + 1].Value;
  407. }
  408. }
  409. return new(groups.Count);
  410. }
  411. else
  412. {
  413. buffer.Span[0] = LuaValue.Nil;
  414. return new(1);
  415. }
  416. });
  417. return new(1);
  418. }
  419. public async ValueTask<int> GSub(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
  420. {
  421. var s = context.GetArgument<string>(0);
  422. var pattern = context.GetArgument<string>(1);
  423. var repl = context.GetArgument(2);
  424. var n_arg = context.HasArgument(3)
  425. ? context.GetArgument<double>(3)
  426. : int.MaxValue;
  427. LuaRuntimeException.ThrowBadArgumentIfNumberIsNotInteger(context.State, "gsub", 4, n_arg);
  428. var n = (int)n_arg;
  429. var regex = StringHelper.ToRegex(pattern);
  430. var matches = regex.Matches(s);
  431. // TODO: reduce allocation
  432. var builder = new StringBuilder();
  433. var lastIndex = 0;
  434. var replaceCount = 0;
  435. for (int i = 0; i < matches.Count; i++)
  436. {
  437. if (replaceCount > n) break;
  438. var match = matches[i];
  439. builder.Append(s.AsSpan()[lastIndex..match.Index]);
  440. replaceCount++;
  441. LuaValue result;
  442. if (repl.TryRead<string>(out var str))
  443. {
  444. result = str.Replace("%%", "%")
  445. .Replace("%0", match.Value);
  446. for (int k = 1; k <= match.Groups.Count; k++)
  447. {
  448. if (replaceCount > n) break;
  449. result = result.Read<string>().Replace($"%{k}", match.Groups[k].Value);
  450. replaceCount++;
  451. }
  452. }
  453. else if (repl.TryRead<LuaTable>(out var table))
  454. {
  455. result = table[match.Groups[1].Value];
  456. }
  457. else if (repl.TryRead<LuaFunction>(out var func))
  458. {
  459. for (int k = 1; k <= match.Groups.Count; k++)
  460. {
  461. context.State.Push(match.Groups[k].Value);
  462. }
  463. using var methodBuffer = new PooledArray<LuaValue>(1024);
  464. await func.InvokeAsync(context with
  465. {
  466. ArgumentCount = match.Groups.Count,
  467. FrameBase = context.Thread.Stack.Count - context.ArgumentCount,
  468. }, methodBuffer.AsMemory(), cancellationToken);
  469. result = methodBuffer[0];
  470. }
  471. else
  472. {
  473. throw new LuaRuntimeException(context.State.GetTraceback(), "bad argument #3 to 'gsub' (string/function/table expected)");
  474. }
  475. if (result.TryRead<string>(out var rs))
  476. {
  477. builder.Append(rs);
  478. }
  479. else if (result.TryRead<double>(out var rd))
  480. {
  481. builder.Append(rd);
  482. }
  483. else if (!result.ToBoolean())
  484. {
  485. builder.Append(match.Value);
  486. replaceCount--;
  487. }
  488. else
  489. {
  490. throw new LuaRuntimeException(context.State.GetTraceback(), $"invalid replacement value (a {result.Type})");
  491. }
  492. lastIndex = match.Index + match.Length;
  493. }
  494. builder.Append(s.AsSpan()[lastIndex..s.Length]);
  495. buffer.Span[0] = builder.ToString();
  496. return 1;
  497. }
  498. public ValueTask<int> Len(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
  499. {
  500. var s = context.GetArgument<string>(0);
  501. buffer.Span[0] = s.Length;
  502. return new(1);
  503. }
  504. public ValueTask<int> Lower(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
  505. {
  506. var s = context.GetArgument<string>(0);
  507. buffer.Span[0] = s.ToLower();
  508. return new(1);
  509. }
  510. public ValueTask<int> Rep(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
  511. {
  512. var s = context.GetArgument<string>(0);
  513. var n_arg = context.GetArgument<double>(1);
  514. var sep = context.HasArgument(2)
  515. ? context.GetArgument<string>(2)
  516. : null;
  517. LuaRuntimeException.ThrowBadArgumentIfNumberIsNotInteger(context.State, "rep", 2, n_arg);
  518. var n = (int)n_arg;
  519. var builder = new ValueStringBuilder(s.Length * n);
  520. for (int i = 0; i < n; i++)
  521. {
  522. builder.Append(s);
  523. if (i != n - 1 && sep != null)
  524. {
  525. builder.Append(sep);
  526. }
  527. }
  528. buffer.Span[0] = builder.ToString();
  529. return new(1);
  530. }
  531. public ValueTask<int> Reverse(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
  532. {
  533. var s = context.GetArgument<string>(0);
  534. using var strBuffer = new PooledArray<char>(s.Length);
  535. var span = strBuffer.AsSpan()[..s.Length];
  536. s.AsSpan().CopyTo(span);
  537. span.Reverse();
  538. buffer.Span[0] = span.ToString();
  539. return new(1);
  540. }
  541. public ValueTask<int> Sub(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
  542. {
  543. var s = context.GetArgument<string>(0);
  544. var i = context.GetArgument<double>(1);
  545. var j = context.HasArgument(2)
  546. ? context.GetArgument<double>(2)
  547. : -1;
  548. LuaRuntimeException.ThrowBadArgumentIfNumberIsNotInteger(context.State, "sub", 2, i);
  549. LuaRuntimeException.ThrowBadArgumentIfNumberIsNotInteger(context.State, "sub", 3, j);
  550. buffer.Span[0] = StringHelper.Slice(s, (int)i, (int)j).ToString();
  551. return new(1);
  552. }
  553. public ValueTask<int> Upper(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
  554. {
  555. var s = context.GetArgument<string>(0);
  556. buffer.Span[0] = s.ToUpper();
  557. return new(1);
  558. }
  559. }