StringLibrary.cs 23 KB

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