Compare commits
646 Commits
v0.1.0
..
specialize
| Author | SHA1 | Date | |
|---|---|---|---|
| dc72d75f3e | |||
| f77775b59e | |||
| b52f6c9823 | |||
| 12244064bb | |||
| 6430ee0bfa | |||
| cf530eba46 | |||
| 64fe47161f | |||
| 4dff8eaa4d | |||
| 2dbd29a565 | |||
| f27a07d31a | |||
| 644515958c | |||
| 8683be35f6 | |||
| dc1740d473 | |||
| 11f789ef11 | |||
| 74d21b367f | |||
| 6617e7e73d | |||
| 3dbca20bc9 | |||
| 85c0658984 | |||
| 772d169b52 | |||
| b4ec220f7e | |||
| 3694ae88f6 | |||
| 19751e8a51 | |||
| 925f214125 | |||
| 39f893ad99 | |||
| c871a61015 | |||
| d0d001625c | |||
| 64941b99e2 | |||
| ed02a1f192 | |||
| 4d7c9ddac7 | |||
| feb1830dcc | |||
| 1c33d6ce20 | |||
| 3000471a12 | |||
| 1f33a6a476 | |||
| 2700aad5d2 | |||
| 7ccaa2744e | |||
| df4bb061f8 | |||
| 9e81ce1c33 | |||
| a23850f29b | |||
| 76f53c857b | |||
| 85f5d567e4 | |||
| bd5cba0b0b | |||
| cd54472d03 | |||
| cc0d1a90ff | |||
| 4984dcb5e9 | |||
| 86632bc190 | |||
| d25eec183f | |||
| e96746311c | |||
| 62acca2b68 | |||
| 476492a85c | |||
| ee9b902222 | |||
| fa56f48daf | |||
| f34c9c332f | |||
| a0d685d482 | |||
| 4a5ae9e81e | |||
| 7e2fa1bc80 | |||
| 40cfb6f458 | |||
| 1230ca485e | |||
| 69ab7e10d1 | |||
| fa07960695 | |||
| 8be357dfa1 | |||
| a93b34d984 | |||
| 9c24fbeaad | |||
| f6b38c33b7 | |||
| 773f10110c | |||
| 618f0181ac | |||
| f3bb20ea79 | |||
| b0d6c62255 | |||
| b202375414 | |||
| 250e073408 | |||
| 11f022ab09 | |||
| 840df4a229 | |||
| c2d1fd86e5 | |||
| 238a6fef7d | |||
| 67852cf007 | |||
| d276c42adc | |||
| 95c701b253 | |||
| 3db329a512 | |||
| 45ea20024b | |||
| ea225df3ed | |||
| 4545a3e94b | |||
| 3f2bb0b363 | |||
| 201d5dd422 | |||
| 1e80267558 | |||
| 931f02a519 | |||
| a533331aee | |||
| 466faaab9f | |||
| e443b4fdb8 | |||
| 2b1c52dddd | |||
| 21445f5170 | |||
| 9ba52996d8 | |||
| 6fe332a869 | |||
| 32c1a8d372 | |||
| ee102a3528 | |||
| 9b67e40640 | |||
| dca2103910 | |||
| a713a96e69 | |||
| a7b50eb8f1 | |||
| 24b62ebe61 | |||
| 9bc4641a49 | |||
| b86b890b8d | |||
| 080baa8574 | |||
| 0537b928df | |||
| 2eff2d082a | |||
| 59cddbc573 | |||
| 9e122af5fc | |||
| ed1f9ed9de | |||
| 466bfe8664 | |||
| e1f035461b | |||
| 84f9e9bceb | |||
| ca41df4a59 | |||
| f2378983d9 | |||
| 37714006b6 | |||
| 275e366c17 | |||
| 18af62d3ea | |||
| af00765ca0 | |||
| 5f877c52fd | |||
| 92b16cad91 | |||
| 4a4c2c2a5f | |||
| 5d905981cf | |||
| 7ccacf158e | |||
| 739ceda96c | |||
| 32da85ab11 | |||
| 18d45c446b | |||
| bcd5333b03 | |||
| e5255a5be2 | |||
| cf288a51c5 | |||
| 72a1afdcb2 | |||
| 2714786b37 | |||
| 51d78a5f0c | |||
| 78389c641a | |||
| c3fc668f27 | |||
| 7f016efe03 | |||
| 269b742eb2 | |||
| 7d8ea80dc3 | |||
| 6165b9454f | |||
| 2ddbf6be6d | |||
| da21b0aecf | |||
| 829c005784 | |||
| b24eb93e8e | |||
| 7dc5550057 | |||
| 9a436c7eeb | |||
| 72c999ecbf | |||
| e5a091a092 | |||
| 317b36b24b | |||
| 636a75f316 | |||
| 390927a0cd | |||
| 3f23ab97e0 | |||
| 47611ff9ea | |||
| f4ac7f7bfa | |||
| e75f23188d | |||
| 6c8adbcb17 | |||
| ffc7d3ba6e | |||
| 4efec6b76a | |||
| 0fcf06e374 | |||
| 1d332cd112 | |||
| 9d3a912da0 | |||
| 1da2fc7e28 | |||
| 17299c937b | |||
| 1bae751a45 | |||
| 8a8d1233bb | |||
| ad538d97c9 | |||
| 43fc2fa552 | |||
| dd5837651d | |||
| a0d031abec | |||
| a25f636a07 | |||
| a3b7e1e353 | |||
| bfeb32c9ce | |||
| 0703eeb262 | |||
| d458ddf4d4 | |||
| 4038ec3dae | |||
| 5b92184e42 | |||
| c6f117c45d | |||
| e78ccff9a4 | |||
| b8da9d1854 | |||
| e5d63aa8fc | |||
| ac2d6e2030 | |||
| fcc91f2618 | |||
| 8b34e54764 | |||
| ebffe6db83 | |||
| 9ec4e86883 | |||
| 93a7b0d77d | |||
| 3e8b8db786 | |||
| 8957a768ef | |||
| fad86a5f24 | |||
| 548b128e67 | |||
| a577df2dbb | |||
| cb678e6221 | |||
| 939f889666 | |||
| f9f9ccb777 | |||
| c6892fcf5a | |||
| 844c9093a2 | |||
| 37d06dabcf | |||
| 1718142ede | |||
| ad64e5d2e2 | |||
| 00b2f776a9 | |||
| b8df31de84 | |||
| 16a336b4f3 | |||
| 590d674153 | |||
| 9a1cfcdd8e | |||
| 590d7faf65 | |||
| de035f0fed | |||
| 04925e4882 | |||
| 3760527218 | |||
| fa7ee6461a | |||
| fbf01f7683 | |||
| a0548e793c | |||
| 1fafb71fd9 | |||
| d8be04d4a8 | |||
| 21d3e85fcc | |||
| 93a74fca35 | |||
| a1c9b661b4 | |||
| 87b9d1cf98 | |||
| 90f3b658c6 | |||
| c35bcc5519 | |||
| f698c102c7 | |||
| 2cee819ce4 | |||
| bf051f1718 | |||
| c77f1d815c | |||
| d24deebee3 | |||
| 4526154571 | |||
| 978143ce99 | |||
| 7f9822db35 | |||
| 052233e858 | |||
| 629a2475a9 | |||
| 46573551f1 | |||
| 1f41c556e8 | |||
| 9ac08febd2 | |||
| 2341b4df00 | |||
| 6e79ce63c2 | |||
| e2a07a3b92 | |||
| 0dad1a950c | |||
| 27f0aeee30 | |||
| 721fa81f2e | |||
| f6a13d6e05 | |||
| 2660bb8426 | |||
| 84282bbfd3 | |||
| 0982fd5f1f | |||
| 7dbf7554c4 | |||
| 2790964270 | |||
| 3488a91eff | |||
| 0e8fd64203 | |||
| 70d41bd750 | |||
| a197513ce7 | |||
| bd8df24646 | |||
| 89052d60b4 | |||
| 9fa2fd413d | |||
| b1e11f82a9 | |||
| 165f65408d | |||
| 540c2a7b59 | |||
| a466f0ca79 | |||
| 736a75748b | |||
| ca12c0670d | |||
| 0ee0fe7f7c | |||
| b123c357c5 | |||
| 94ad175728 | |||
| 1e8b0dc3c9 | |||
| 7300b6a97b | |||
| aae4656c64 | |||
| 44f7a7aead | |||
| bac65cc530 | |||
| 91d7afbc0a | |||
| 7b4d82a939 | |||
| 2ab0f8c733 | |||
| b96c535061 | |||
| fd961100c1 | |||
| 1c7e9fe3af | |||
| 07aa85ea0b | |||
| d54ad15d16 | |||
| abe1005d7a | |||
| b4bb91fc13 | |||
| c9a09d8695 | |||
| 3430b0f086 | |||
| a713a3eccc | |||
| 652b9f8232 | |||
| ba1b12be14 | |||
| 2e01f733df | |||
| 1bd9461acb | |||
| 5b4e7e5dcc | |||
| b4905040a8 | |||
| 5c66c78bc5 | |||
| f9ba08244d | |||
| e6908614ee | |||
| a7448fe8de | |||
| 65ca806488 | |||
| 5c94d86029 | |||
| b76eb62117 | |||
| 196ce3a1f6 | |||
| 9f8f82dfe8 | |||
| 661484ae7e | |||
| 34de94e6a8 | |||
| 88263a05cc | |||
| 1dbe20e76c | |||
| 05bf3807d3 | |||
| 06838de5d2 | |||
| db62263e3e | |||
| 2d866e3fae | |||
| 100799f7b7 | |||
| ecd155a62f | |||
| bcacc71a18 | |||
| 16c9a8bdc0 | |||
| f99d6bbca1 | |||
| 8784f9c73a | |||
| a60e466129 | |||
| 44aed552fd | |||
| 1479e10663 | |||
| 9ba7363552 | |||
| 96ff402934 | |||
| 249d0eaf46 | |||
| 19eb8cf036 | |||
| c5fbd3eba6 | |||
| 9ccd9bbc7a | |||
| e7d1a179ae | |||
| 71a8bd4c61 | |||
| 34782191ba | |||
| 7fbde32684 | |||
| 82a6a1977d | |||
| cc3100c329 | |||
| f1ba6388fb | |||
| d05497900e | |||
| e29a498ed5 | |||
| 2b8e33f503 | |||
| d3c92c5999 | |||
| 71c324cf7b | |||
| 4c840f1b8b | |||
| d1e0fc37ce | |||
| 947ab3f90a | |||
| e9e8265313 | |||
| a30fd2239c | |||
| 323fe5d063 | |||
| 24d4446802 | |||
| 5060c72d94 | |||
| 0a459e938d | |||
| e872682c78 | |||
| 145b18309a | |||
| 8e8d2a6aad | |||
| 3f7178ffd6 | |||
| 9fd5922321 | |||
| 610cf85ed6 | |||
| 99f8a2a010 | |||
| 556d384d4c | |||
| eb7280e4a7 | |||
| 7ee1118b4b | |||
| a12e102214 | |||
| ad60b7e437 | |||
| 3503483c73 | |||
| d2d17bccec | |||
| 76a94674c9 | |||
| 80f8b7660b | |||
| 6f6ca41621 | |||
| c4efb7477c | |||
| 903d9455db | |||
| a89a075e1b | |||
| 5e74bb91ea | |||
| 3a4d7af89e | |||
| 8a362ad712 | |||
| 5edf9acd3e | |||
| e95df67ba3 | |||
| bef0f57967 | |||
| e87c92d4f4 | |||
| 8fe62057ea | |||
| 5f42261979 | |||
| 75654e60b8 | |||
| 091e2dc498 | |||
| 095a905e04 | |||
| ec312409d3 | |||
| 26fd12ff54 | |||
| b40204d36a | |||
| 4d5afd743f | |||
| 3ded2e09ee | |||
| 781fbae71e | |||
| 68063a447e | |||
| 84da2c4a25 | |||
| dba45d427f | |||
| 728039f679 | |||
| 1d8903f1d0 | |||
| 65b27e6823 | |||
| 6ea91ef590 | |||
| 51edd0ca49 | |||
| d95bfe020e | |||
| 63909f0a90 | |||
| f9070d3b40 | |||
| 405d48dc28 | |||
| 690ec00a4b | |||
| bef2d19cb0 | |||
| e1803f96f6 | |||
| d9a27b8052 | |||
| ad2aec1dcc | |||
| 489c49b1b4 | |||
| 27c6b39a13 | |||
| 539dd095b3 | |||
| b56e1b27b4 | |||
| 19cbd226da | |||
| 0a1666a81f | |||
| aa79e12a97 | |||
| 81a861c69d | |||
| 78b76feda6 | |||
| 90d6f96e9e | |||
| e33f654429 | |||
| 4edab6691b | |||
| c2dbbc24a9 | |||
| 14d3ac30da | |||
| 5c5490133d | |||
| 216c9ec838 | |||
| a295f02a64 | |||
| dbe63ccdd0 | |||
| 603baefff9 | |||
| c01d1270ff | |||
| 66540cf1fc | |||
| 05bcc0fb0d | |||
| acdc450948 | |||
| 778c285afa | |||
| a1e8a8d702 | |||
| 03c6bf4172 | |||
| a1b12e18b7 | |||
| 4874e8477b | |||
| 9bf0212445 | |||
| 0131db6d73 | |||
| 861c4734ac | |||
| b8b5e76965 | |||
| 4e9e0ee19b | |||
| 8c31c2ec65 | |||
| 6d858869d3 | |||
| 1916042ba2 | |||
| a410399d2c | |||
| 878c11e70e | |||
| 19ece5dc77 | |||
| d01db88be9 | |||
| 2009e44b6f | |||
| 690dbc9ee7 | |||
| 16398bac15 | |||
| 1d6b12b7cb | |||
| 9c1b4e331f | |||
| 4692b8f9ba | |||
| 69d355db53 | |||
| ef23ce9e92 | |||
| 4a000a21a4 | |||
| fe7536c3de | |||
| e94d595cd4 | |||
| 0d5a6db8dd | |||
| a60c71373e | |||
| 5ccdfb18c7 | |||
| 40ecdac242 | |||
| 26ae43fdee | |||
| 048765b449 | |||
| 5c26a6ff6f | |||
| 685a1f1cb7 | |||
| 23f644976a | |||
| 64bc956d5e | |||
| 53be957dac | |||
| 97253b98df | |||
| 76c552dcd7 | |||
| fe206efb84 | |||
| e32a2e0474 | |||
| f6e7596e8d | |||
| 25e50242f6 | |||
| 62e2d802ed | |||
| fee7787d3f | |||
| 3b00596b2e | |||
| 13d49d4606 | |||
| 7e6e4b1314 | |||
| 3616783228 | |||
| d0ec4317d3 | |||
| 22139eb546 | |||
| c9506ee963 | |||
| 3a6d01f7a0 | |||
| d1fa2118c1 | |||
| a1f048ba24 | |||
| ee2c0b51cf | |||
| 439fbba1f8 | |||
| 017119f7a7 | |||
| ce7be745f0 | |||
| d464759235 | |||
| 7cb988051d | |||
| 3ddb37c944 | |||
| f7f14983c3 | |||
| 45932ad32d | |||
| 67b7b944a8 | |||
| 31055c2ff0 | |||
| 5a62685873 | |||
| d05a14897c | |||
| 0599275eb9 | |||
| 0049ab3dc4 | |||
| bfe4a7e160 | |||
| e6271032cc | |||
| 887411a2a8 | |||
| 31c735e72c | |||
| 06484b677b | |||
| de2e921d55 | |||
| 7f292800de | |||
| 923742e542 | |||
| 65ad89c1a7 | |||
| 64ff1ea4d5 | |||
| b39f6ef1f9 | |||
| c187221f01 | |||
| 8e6ab94eec | |||
| 8d9c606c69 | |||
| 288bc57940 | |||
| e3b2497729 | |||
| 1a8565204c | |||
| e58cfd32d4 | |||
| a2ae216b47 | |||
| 8645be8dc7 | |||
| 99b9371c53 | |||
| 92c565e02b | |||
| 6e26017b00 | |||
| 9d93af61de | |||
| 4d8fb95ffe | |||
| 0e41db2176 | |||
| afca7f3334 | |||
| d6a90e60ed | |||
| fe63e9f76d | |||
| 7f50e4c339 | |||
| a402e618c3 | |||
| 2df083520a | |||
| 8176e30b38 | |||
| 14c964fc02 | |||
| f963bc320f | |||
| 0488b850c6 | |||
| 346e676fa2 | |||
| 6d743bb19f | |||
| fa1c2ab68c | |||
| a6c6ad1f5f | |||
| ab7a652912 | |||
| 3102b98900 | |||
| f0cae62430 | |||
| 56c6106477 | |||
| 7d69e5a5c5 | |||
| 07d0c2e4d3 | |||
| 6b9002d8f9 | |||
| 5753e884d0 | |||
| d467309bdd | |||
| 821a80e635 | |||
| dd4c4ffc2b | |||
| da703daafe | |||
| f58048cec0 | |||
| 440592fa85 | |||
| f4f2456dcd | |||
| a77f30ea80 | |||
| d61c80733b | |||
| 894e775e38 | |||
| 8e75093380 | |||
| cf5ad6a245 | |||
| 8fc7451ffc | |||
| 9defd66d3c | |||
| 6adf8057ed | |||
| 36e1197190 | |||
| 6dd2de38a9 | |||
| 209315c2af | |||
| 41a8959f14 | |||
| 16a681db2a | |||
| 9f36448571 | |||
| 222e90a7d3 | |||
| a8327d781a | |||
| 61449e9d32 | |||
| 48c977fb58 | |||
| 42e7853ef6 | |||
| 1f3d0e03c3 | |||
| 36d65b681a | |||
| a56707c85f | |||
| 4b47f52cb0 | |||
| 2f2f28631b | |||
| 543444f747 | |||
| b814e1a94f | |||
| 1fe62f3000 | |||
| 709382e9c1 | |||
| 71e7762db5 | |||
| 34da10d880 | |||
| db15f8a481 | |||
| 8ef71920bd | |||
| fa055bcbba | |||
| 7337a63f5a | |||
| 21af3aacfe | |||
| 66e7f06e7d | |||
| d9de45b5b5 | |||
| d9e8f54d1c | |||
| 7f678451a8 | |||
| 081f3db916 | |||
| 2811a1a3c9 | |||
| 7f30fba1e6 | |||
| 12e974f892 | |||
| c81f1892c2 | |||
| a98788e0d7 | |||
| b74544d345 | |||
| 27416cc1b9 | |||
| 04b60e4f8d | |||
| 9942463786 | |||
| fb5423fba2 | |||
| a2495b4806 | |||
| e118479061 | |||
| c0c5d65185 | |||
| 7c63fff960 | |||
| bcbaee1079 | |||
| f15dd550e8 | |||
| 5ad4cb7120 | |||
| 65684e6bb0 | |||
| 68d2a60b37 | |||
| bf549a2194 | |||
| aa194b5c41 | |||
| 14e2d94bdd | |||
| 1f8a8cbc06 | |||
| 6db660fed5 | |||
| 7d9a3c25bd | |||
| dd04a2f3cd | |||
| e493544dfd | |||
| e9ae961088 | |||
| c1ad095e9b | |||
| 86a7f2508e | |||
| 713478a34e | |||
| 5dd3b53635 | |||
| 262211488d | |||
| abdecb7be7 | |||
| f69e0b0837 | |||
| cd055fa448 | |||
| c9ea292f59 | |||
| 7b208738bc | |||
| c8b5633273 | |||
| b28c2d0c9e | |||
| 1c0f7f552c | |||
| 0773148832 | |||
| fbf99a1816 | |||
| 1cfdab9cee | |||
| dc20c454d7 | |||
| 0c4e891f3e | |||
| b1d602f733 | |||
| a4d623ad05 | |||
| 4fdde9794a | |||
| 3085454477 | |||
| 01609e0ab7 | |||
| a34fc5f051 | |||
| e8d5dbf787 | |||
| 5ffe2e5565 | |||
| 278c4d97ec | |||
| c743c90315 | |||
| 8081f3cc09 | |||
| 72f17747a0 | |||
| 979a055512 | |||
| def5433558 | |||
| c163b3f68b | |||
| bbe45c63f2 | |||
| 40a44dc51f | |||
| 2ba6587bf3 | |||
| 72d57d8477 | |||
| 0f6008f46e | |||
| be268e4049 | |||
| 53005a205f | |||
| 34d60ec92f |
@@ -0,0 +1,2 @@
|
||||
cmd/tomll/tomll
|
||||
cmd/tomljson/tomljson
|
||||
@@ -0,0 +1,3 @@
|
||||
* text=auto
|
||||
|
||||
benchmark/benchmark.toml text eol=lf
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior. Including TOML files.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen, if other than "should work".
|
||||
|
||||
**Versions**
|
||||
- go-toml: version (git sha)
|
||||
- go: version
|
||||
- operating system: e.g. macOS, Windows, Linux
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here that you think may help to diagnose.
|
||||
@@ -0,0 +1,19 @@
|
||||
<!--
|
||||
|
||||
Thank you for your pull request!
|
||||
|
||||
Please read the Code changes section of the CONTRIBUTING.md file,
|
||||
and make sure you have followed the instructions.
|
||||
|
||||
https://github.com/pelletier/go-toml/blob/v2/CONTRIBUTING.md#code-changes
|
||||
|
||||
-->
|
||||
|
||||
Explanation of what this pull request does.
|
||||
|
||||
More detailed description of the decisions being made and the reasons why (if
|
||||
the patch is non-trivial).
|
||||
|
||||
---
|
||||
|
||||
Paste `benchstat` results here
|
||||
@@ -0,0 +1,17 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: gomod
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 10
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 10
|
||||
- package-ecosystem: docker
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 10
|
||||
@@ -0,0 +1,20 @@
|
||||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- build
|
||||
categories:
|
||||
- title: What's new
|
||||
labels:
|
||||
- feature
|
||||
- title: Performance
|
||||
labels:
|
||||
- performance
|
||||
- title: Fixed bugs
|
||||
labels:
|
||||
- bug
|
||||
- title: Documentation
|
||||
labels:
|
||||
- doc
|
||||
- title: Other changes
|
||||
labels:
|
||||
- "*"
|
||||
@@ -0,0 +1,67 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, v2 ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
- cron: '26 19 * * 0'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'go' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
@@ -0,0 +1,20 @@
|
||||
name: coverage
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- v2
|
||||
|
||||
jobs:
|
||||
report:
|
||||
runs-on: "ubuntu-latest"
|
||||
name: report
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@master
|
||||
with:
|
||||
go-version: 1.16
|
||||
- name: Run tests with coverage
|
||||
run: ./ci.sh coverage -d "${GITHUB_BASE_REF-HEAD}"
|
||||
@@ -0,0 +1,25 @@
|
||||
name: test
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- v2
|
||||
pull_request:
|
||||
branches:
|
||||
- v2
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ 'ubuntu-latest', 'windows-latest', 'macos-latest']
|
||||
go: [ '1.16', '1.17' ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: ${{ matrix.go }}/${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Setup go ${{ matrix.go }}
|
||||
uses: actions/setup-go@master
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
- name: Run unit tests
|
||||
run: go test -race ./...
|
||||
@@ -0,0 +1,5 @@
|
||||
test_program/test_program_bin
|
||||
fuzz/
|
||||
cmd/tomll/tomll
|
||||
cmd/tomljson/tomljson
|
||||
cmd/tomltestgen/tomltestgen
|
||||
@@ -0,0 +1,84 @@
|
||||
[service]
|
||||
golangci-lint-version = "1.39.0"
|
||||
|
||||
[linters-settings.wsl]
|
||||
allow-assign-and-anything = true
|
||||
|
||||
[linters-settings.exhaustive]
|
||||
default-signifies-exhaustive = true
|
||||
|
||||
[linters]
|
||||
disable-all = true
|
||||
enable = [
|
||||
"asciicheck",
|
||||
"bodyclose",
|
||||
"cyclop",
|
||||
"deadcode",
|
||||
"depguard",
|
||||
"dogsled",
|
||||
"dupl",
|
||||
"durationcheck",
|
||||
"errcheck",
|
||||
"errorlint",
|
||||
"exhaustive",
|
||||
# "exhaustivestruct",
|
||||
"exportloopref",
|
||||
"forbidigo",
|
||||
# "forcetypeassert",
|
||||
"funlen",
|
||||
"gci",
|
||||
# "gochecknoglobals",
|
||||
"gochecknoinits",
|
||||
"gocognit",
|
||||
"goconst",
|
||||
"gocritic",
|
||||
"gocyclo",
|
||||
"godot",
|
||||
"godox",
|
||||
# "goerr113",
|
||||
"gofmt",
|
||||
"gofumpt",
|
||||
"goheader",
|
||||
"goimports",
|
||||
"golint",
|
||||
"gomnd",
|
||||
# "gomoddirectives",
|
||||
"gomodguard",
|
||||
"goprintffuncname",
|
||||
"gosec",
|
||||
"gosimple",
|
||||
"govet",
|
||||
# "ifshort",
|
||||
"importas",
|
||||
"ineffassign",
|
||||
"lll",
|
||||
"makezero",
|
||||
"misspell",
|
||||
"nakedret",
|
||||
"nestif",
|
||||
"nilerr",
|
||||
# "nlreturn",
|
||||
"noctx",
|
||||
"nolintlint",
|
||||
#"paralleltest",
|
||||
"prealloc",
|
||||
"predeclared",
|
||||
"revive",
|
||||
"rowserrcheck",
|
||||
"sqlclosecheck",
|
||||
"staticcheck",
|
||||
"structcheck",
|
||||
"stylecheck",
|
||||
# "testpackage",
|
||||
"thelper",
|
||||
"tparallel",
|
||||
"typecheck",
|
||||
"unconvert",
|
||||
"unparam",
|
||||
"unused",
|
||||
"varcheck",
|
||||
"wastedassign",
|
||||
"whitespace",
|
||||
# "wrapcheck",
|
||||
# "wsl"
|
||||
]
|
||||
@@ -1 +0,0 @@
|
||||
language: go
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
# Contributing
|
||||
|
||||
Thank you for your interest in go-toml! We appreciate you considering
|
||||
contributing to go-toml!
|
||||
|
||||
The main goal is the project is to provide an easy-to-use and efficient TOML
|
||||
implementation for Go that gets the job done and gets out of your way – dealing
|
||||
with TOML is probably not the central piece of your project.
|
||||
|
||||
As the single maintainer of go-toml, time is scarce. All help, big or small, is
|
||||
more than welcomed!
|
||||
|
||||
## Ask questions
|
||||
|
||||
Any question you may have, somebody else might have it too. Always feel free to
|
||||
ask them on the [discussion board][discussions]. We will try to answer them as
|
||||
clearly and quickly as possible, time permitting.
|
||||
|
||||
Asking questions also helps us identify areas where the documentation needs
|
||||
improvement, or new features that weren't envisioned before. Sometimes, a
|
||||
seemingly innocent question leads to the fix of a bug. Don't hesitate and ask
|
||||
away!
|
||||
|
||||
[discussions]: https://github.com/pelletier/go-toml/discussions
|
||||
|
||||
## Improve the documentation
|
||||
|
||||
The best way to share your knowledge and experience with go-toml is to improve
|
||||
the documentation. Fix a typo, clarify an interface, add an example, anything
|
||||
goes!
|
||||
|
||||
The documentation is present in the [README][readme] and thorough the source
|
||||
code. On release, it gets updated on [pkg.go.dev][pkg.go.dev]. To make a change
|
||||
to the documentation, create a pull request with your proposed changes. For
|
||||
simple changes like that, the easiest way to go is probably the "Fork this
|
||||
project and edit the file" button on Github, displayed at the top right of the
|
||||
file. Unless it's a trivial change (for example a typo), provide a little bit of
|
||||
context in your pull request description or commit message.
|
||||
|
||||
## Report a bug
|
||||
|
||||
Found a bug! Sorry to hear that :(. Help us and other track them down and fix by
|
||||
reporting it. [File a new bug report][bug-report] on the [issues
|
||||
tracker][issues-tracker]. The template should provide enough guidance on what to
|
||||
include. When in doubt: add more details! By reducing ambiguity and providing
|
||||
more information, it decreases back and forth and saves everyone time.
|
||||
|
||||
## Code changes
|
||||
|
||||
Want to contribute a patch? Very happy to hear that!
|
||||
|
||||
First, some high-level rules:
|
||||
|
||||
- A short proposal with some POC code is better than a lengthy piece of text
|
||||
with no code. Code speaks louder than words. That being said, bigger changes
|
||||
should probably start with a [discussion][discussions].
|
||||
- No backward-incompatible patch will be accepted unless discussed. Sometimes
|
||||
it's hard, but we try not to break people's programs unless we absolutely have
|
||||
to.
|
||||
- If you are writing a new feature or extending an existing one, make sure to
|
||||
write some documentation.
|
||||
- Bug fixes need to be accompanied with regression tests.
|
||||
- New code needs to be tested.
|
||||
- Your commit messages need to explain why the change is needed, even if already
|
||||
included in the PR description.
|
||||
|
||||
It does sound like a lot, but those best practices are here to save time overall
|
||||
and continuously improve the quality of the project, which is something everyone
|
||||
benefits from.
|
||||
|
||||
### Get started
|
||||
|
||||
The fairly standard code contribution process looks like that:
|
||||
|
||||
1. [Fork the project][fork].
|
||||
2. Make your changes, commit on any branch you like.
|
||||
3. [Open up a pull request][pull-request]
|
||||
4. Review, potential ask for changes.
|
||||
5. Merge.
|
||||
|
||||
Feel free to ask for help! You can create draft pull requests to gather
|
||||
some early feedback!
|
||||
|
||||
### Run the tests
|
||||
|
||||
You can run tests for go-toml using Go's test tool: `go test -race ./...`.
|
||||
|
||||
During the pull request process, all tests will be ran on Linux, Windows, and
|
||||
MacOS on the last two versions of Go.
|
||||
|
||||
However, given GitHub's new policy to _not_ run Actions on pull requests until a
|
||||
maintainer clicks on button, it is highly recommended that you run them locally
|
||||
as you make changes.
|
||||
|
||||
### Check coverage
|
||||
|
||||
We use `go tool cover` to compute test coverage. Most code editors have a way to
|
||||
run and display code coverage, but at the end of the day, we do this:
|
||||
|
||||
```
|
||||
go test -covermode=atomic -coverprofile=coverage.out
|
||||
go tool cover -func=coverage.out
|
||||
```
|
||||
|
||||
and verify that the overall percentage of tested code does not go down. This is
|
||||
a requirement. As a rule of thumb, all lines of code touched by your changes
|
||||
should be covered. On Unix you can use `./ci.sh coverage -d v2` to check if your
|
||||
code lowers the coverage.
|
||||
|
||||
### Verify performance
|
||||
|
||||
Go-toml aims to stay efficient. We rely on a set of scenarios executed with Go's
|
||||
builtin benchmark systems. Because of their noisy nature, containers provided by
|
||||
Github Actions cannot be reliably used for benchmarking. As a result, you are
|
||||
responsible for checking that your changes do not incur a performance penalty.
|
||||
You can run their following to execute benchmarks:
|
||||
|
||||
```
|
||||
go test ./... -bench=. -count=10
|
||||
```
|
||||
|
||||
Benchmark results should be compared against each other with
|
||||
[benchstat][benchstat]. Typical flow looks like this:
|
||||
|
||||
1. On the `v2` branch, run `go test ./... -bench=. -count 10` and save output to
|
||||
a file (for example `old.txt`).
|
||||
2. Make some code changes.
|
||||
3. Run `go test ....` again, and save the output to an other file (for example
|
||||
`new.txt`).
|
||||
4. Run `benchstat old.txt new.txt` to check that time/op does not go up in any
|
||||
test.
|
||||
|
||||
On Unix you can use `./ci.sh benchmark -d v2` to verify how your code impacts
|
||||
performance.
|
||||
|
||||
It is highly encouraged to add the benchstat results to your pull request
|
||||
description. Pull requests that lower performance will receive more scrutiny.
|
||||
|
||||
[benchstat]: https://pkg.go.dev/golang.org/x/perf/cmd/benchstat
|
||||
|
||||
### Style
|
||||
|
||||
Try to look around and follow the same format and structure as the rest of the
|
||||
code. We enforce using `go fmt` on the whole code base.
|
||||
|
||||
---
|
||||
|
||||
## Maintainers-only
|
||||
|
||||
### Merge pull request
|
||||
|
||||
Checklist:
|
||||
|
||||
- Passing CI.
|
||||
- Does not introduce backward-incompatible changes (unless discussed).
|
||||
- Has relevant doc changes.
|
||||
- Benchstat does not show performance regression.
|
||||
|
||||
1. Merge using "squash and merge".
|
||||
2. Make sure to edit the commit message to keep all the useful information
|
||||
nice and clean.
|
||||
3. Make sure the commit title is clear and contains the PR number (#123).
|
||||
|
||||
### New release
|
||||
|
||||
1. Go to [releases][releases]. Click on "X commits to master since this
|
||||
release".
|
||||
2. Make note of all the changes. Look for backward incompatible changes,
|
||||
new features, and bug fixes.
|
||||
3. Pick the new version using the above and semver.
|
||||
4. Create a [new release][new-release].
|
||||
5. Follow the same format as [1.1.0][release-110].
|
||||
|
||||
[issues-tracker]: https://github.com/pelletier/go-toml/issues
|
||||
[bug-report]: https://github.com/pelletier/go-toml/issues/new?template=bug_report.md
|
||||
[pkg.go.dev]: https://pkg.go.dev/github.com/pelletier/go-toml
|
||||
[readme]: ./README.md
|
||||
[fork]: https://help.github.com/articles/fork-a-repo
|
||||
[pull-request]: https://help.github.com/en/articles/creating-a-pull-request
|
||||
[releases]: https://github.com/pelletier/go-toml/releases
|
||||
[new-release]: https://github.com/pelletier/go-toml/releases/new
|
||||
[release-110]: https://github.com/pelletier/go-toml/releases/tag/v1.1.0
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 - 2021 Thomas Pelletier, Eric Anderton
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,80 +1,427 @@
|
||||
# go-toml
|
||||
# go-toml v2
|
||||
|
||||
Go library for the [TOML](https://github.com/mojombo/toml) format.
|
||||
Go library for the [TOML](https://toml.io/en/) format.
|
||||
|
||||
This library supports TOML version
|
||||
[v0.1.0](https://github.com/mojombo/toml/blob/master/versions/toml-v0.1.0.md)
|
||||
This library supports [TOML v1.0.0](https://toml.io/en/v1.0.0).
|
||||
|
||||
[](https://travis-ci.org/pelletier/go-toml)
|
||||
## Development status
|
||||
|
||||
## Import
|
||||
This is the upcoming major version of go-toml. It is currently in active
|
||||
development. As of release v2.0.0-beta.1, the library has reached feature parity
|
||||
with v1, and fixes a lot known bugs and performance issues along the way.
|
||||
|
||||
import "github.com/pelletier/go-toml"
|
||||
If you do not need the advanced document editing features of v1, you are
|
||||
encouraged to try out this version.
|
||||
|
||||
## Usage
|
||||
[👉 Roadmap for v2](https://github.com/pelletier/go-toml/discussions/506)
|
||||
|
||||
Say you have a TOML file that looks like this:
|
||||
[🐞 Bug Reports](https://github.com/pelletier/go-toml/issues)
|
||||
|
||||
```toml
|
||||
[postgres]
|
||||
user = "pelletier"
|
||||
password = "mypassword"
|
||||
```
|
||||
|
||||
Read the username and password like this:
|
||||
|
||||
```go
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/pelletier/go-toml"
|
||||
)
|
||||
|
||||
config, err := toml.LoadFile("config.toml")
|
||||
if err != nil {
|
||||
fmt.Println("Error ", err.Error())
|
||||
} else {
|
||||
// retrieve data directly
|
||||
user := config.Get("postgres.user").(string)
|
||||
password := config.Get("postgres.password").(string)
|
||||
|
||||
// or using an intermediate object
|
||||
configTree := config.Get("postgres").(*toml.TomlTree)
|
||||
user = configTree.Get("user").(string)
|
||||
password = configTree.Get("password").(string)
|
||||
fmt.Println("User is ", user, ". Password is ", password)
|
||||
}
|
||||
```
|
||||
[💬 Anything else](https://github.com/pelletier/go-toml/discussions)
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation is available at
|
||||
[godoc.org](http://godoc.org/github.com/pelletier/go-toml).
|
||||
Full API, examples, and implementation notes are available in the Go documentation.
|
||||
|
||||
## Contribute
|
||||
[](https://pkg.go.dev/github.com/pelletier/go-toml/v2)
|
||||
|
||||
Feel free to report bugs and patches using GitHub's pull requests system on
|
||||
[pelletier/go-toml](https://github.com/pelletier/go-toml). Any feedback would be
|
||||
much appreciated!
|
||||
## Import
|
||||
|
||||
```go
|
||||
import "github.com/pelletier/go-toml/v2"
|
||||
```
|
||||
|
||||
See [Modules](#Modules).
|
||||
|
||||
## Features
|
||||
|
||||
### Stdlib behavior
|
||||
|
||||
As much as possible, this library is designed to behave similarly as the
|
||||
standard library's `encoding/json`.
|
||||
|
||||
### Performance
|
||||
|
||||
While go-toml favors usability, it is written with performance in mind. Most
|
||||
operations should not be shockingly slow. See [benchmarks](#benchmarks).
|
||||
|
||||
### Strict mode
|
||||
|
||||
`Decoder` can be set to "strict mode", which makes it error when some parts of
|
||||
the TOML document was not prevent in the target structure. This is a great way
|
||||
to check for typos. [See example in the documentation][strict].
|
||||
|
||||
[strict]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#example-Decoder.SetStrict
|
||||
|
||||
### Contextualized errors
|
||||
|
||||
When decoding errors occur, go-toml returns [`DecodeError`][decode-err]), which
|
||||
contains a human readable contextualized version of the error. For example:
|
||||
|
||||
```
|
||||
2| key1 = "value1"
|
||||
3| key2 = "missing2"
|
||||
| ~~~~ missing field
|
||||
4| key3 = "missing3"
|
||||
5| key4 = "value4"
|
||||
```
|
||||
|
||||
[decode-err]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#DecodeError
|
||||
|
||||
### Local date and time support
|
||||
|
||||
TOML supports native [local date/times][ldt]. It allows to represent a given
|
||||
date, time, or date-time without relation to a timezone or offset. To support
|
||||
this use-case, go-toml provides [`LocalDate`][tld], [`LocalTime`][tlt], and
|
||||
[`LocalDateTime`][tldt]. Those types can be transformed to and from `time.Time`,
|
||||
making them convenient yet unambiguous structures for their respective TOML
|
||||
representation.
|
||||
|
||||
[ldt]: https://toml.io/en/v1.0.0#local-date-time
|
||||
[tld]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalDate
|
||||
[tlt]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalTime
|
||||
[tldt]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalDateTime
|
||||
|
||||
## Getting started
|
||||
|
||||
Given the following struct, let's see how to read it and write it as TOML:
|
||||
|
||||
```go
|
||||
type MyConfig struct {
|
||||
Version int
|
||||
Name string
|
||||
Tags []string
|
||||
}
|
||||
```
|
||||
|
||||
### Unmarshaling
|
||||
|
||||
[`Unmarshal`][unmarshal] reads a TOML document and fills a Go structure with its
|
||||
content. For example:
|
||||
|
||||
```go
|
||||
doc := `
|
||||
version = 2
|
||||
name = "go-toml"
|
||||
tags = ["go", "toml"]
|
||||
`
|
||||
|
||||
var cfg MyConfig
|
||||
err := toml.Unmarshal([]byte(doc), &cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println("version:", cfg.Version)
|
||||
fmt.Println("name:", cfg.Name)
|
||||
fmt.Println("tags:", cfg.Tags)
|
||||
|
||||
// Output:
|
||||
// version: 2
|
||||
// name: go-toml
|
||||
// tags: [go toml]
|
||||
```
|
||||
|
||||
[unmarshal]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#Unmarshal
|
||||
|
||||
### Marshaling
|
||||
|
||||
[`Marshal`][marshal] is the opposite of Unmarshal: it represents a Go structure
|
||||
as a TOML document:
|
||||
|
||||
```go
|
||||
cfg := MyConfig{
|
||||
Version: 2,
|
||||
Name: "go-toml",
|
||||
Tags: []string{"go", "toml"},
|
||||
}
|
||||
|
||||
b, err := toml.Marshal(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(string(b))
|
||||
|
||||
// Output:
|
||||
// Version = 2
|
||||
// Name = 'go-toml'
|
||||
// Tags = ['go', 'toml']
|
||||
```
|
||||
|
||||
[marshal]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#Marshal
|
||||
|
||||
## Benchmarks
|
||||
|
||||
Execution time speedup compared to other Go TOML libraries:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Marshal/HugoFrontMatter-2</td><td>1.9x</td><td>1.9x</td></tr>
|
||||
<tr><td>Marshal/ReferenceFile/map-2</td><td>1.7x</td><td>1.9x</td></tr>
|
||||
<tr><td>Marshal/ReferenceFile/struct-2</td><td>2.4x</td><td>2.6x</td></tr>
|
||||
<tr><td>Unmarshal/HugoFrontMatter-2</td><td>2.9x</td><td>2.5x</td></tr>
|
||||
<tr><td>Unmarshal/ReferenceFile/map-2</td><td>2.7x</td><td>2.6x</td></tr>
|
||||
<tr><td>Unmarshal/ReferenceFile/struct-2</td><td>4.8x</td><td>5.1x</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<details><summary>See more</summary>
|
||||
<p>The table above has the results of the most common use-cases. The table below
|
||||
contains the results of all benchmarks, including unrealistic ones. It is
|
||||
provided for completeness.</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Marshal/SimpleDocument/map-2</td><td>1.7x</td><td>2.1x</td></tr>
|
||||
<tr><td>Marshal/SimpleDocument/struct-2</td><td>2.5x</td><td>2.8x</td></tr>
|
||||
<tr><td>Unmarshal/SimpleDocument/map-2</td><td>4.1x</td><td>3.1x</td></tr>
|
||||
<tr><td>Unmarshal/SimpleDocument/struct-2</td><td>6.4x</td><td>4.3x</td></tr>
|
||||
<tr><td>UnmarshalDataset/example-2</td><td>3.4x</td><td>3.2x</td></tr>
|
||||
<tr><td>UnmarshalDataset/code-2</td><td>2.2x</td><td>2.5x</td></tr>
|
||||
<tr><td>UnmarshalDataset/twitter-2</td><td>2.8x</td><td>2.7x</td></tr>
|
||||
<tr><td>UnmarshalDataset/citm_catalog-2</td><td>2.2x</td><td>2.0x</td></tr>
|
||||
<tr><td>UnmarshalDataset/canada-2</td><td>1.8x</td><td>1.4x</td></tr>
|
||||
<tr><td>UnmarshalDataset/config-2</td><td>4.4x</td><td>2.9x</td></tr>
|
||||
<tr><td>[Geo mean]</td><td>2.8x</td><td>2.6x</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>This table can be generated with <code>./ci.sh benchmark -a -html</code>.</p>
|
||||
</details>
|
||||
|
||||
## Modules
|
||||
|
||||
go-toml uses Go's standard modules system.
|
||||
|
||||
Installation instructions:
|
||||
|
||||
- Go ≥ 1.16: Nothing to do. Use the import in your code. The `go` command deals
|
||||
with it automatically.
|
||||
- Go ≥ 1.13: `GO111MODULE=on go get github.com/pelletier/go-toml/v2`.
|
||||
|
||||
In case of trouble: [Go Modules FAQ][mod-faq].
|
||||
|
||||
[mod-faq]: https://github.com/golang/go/wiki/Modules#why-does-installing-a-tool-via-go-get-fail-with-error-cannot-find-main-module
|
||||
|
||||
## Migrating from v1
|
||||
|
||||
This section describes the differences between v1 and v2, with some pointers on
|
||||
how to get the original behavior when possible.
|
||||
|
||||
### Decoding / Unmarshal
|
||||
|
||||
#### Automatic field name guessing
|
||||
|
||||
When unmarshaling to a struct, if a key in the TOML document does not exactly
|
||||
match the name of a struct field or any of the `toml`-tagged field, v1 tries
|
||||
multiple variations of the key ([code][v1-keys]).
|
||||
|
||||
V2 instead does a case-insensitive matching, like `encoding/json`.
|
||||
|
||||
This could impact you if you are relying on casing to differentiate two fields,
|
||||
and one of them is a not using the `toml` struct tag. The recommended solution
|
||||
is to be specific about tag names for those fields using the `toml` struct tag.
|
||||
|
||||
[v1-keys]: https://github.com/pelletier/go-toml/blob/a2e52561804c6cd9392ebf0048ca64fe4af67a43/marshal.go#L775-L781
|
||||
|
||||
#### Ignore preexisting value in interface
|
||||
|
||||
When decoding into a non-nil `interface{}`, go-toml v1 uses the type of the
|
||||
element in the interface to decode the object. For example:
|
||||
|
||||
```go
|
||||
type inner struct {
|
||||
B interface{}
|
||||
}
|
||||
type doc struct {
|
||||
A interface{}
|
||||
}
|
||||
|
||||
d := doc{
|
||||
A: inner{
|
||||
B: "Before",
|
||||
},
|
||||
}
|
||||
|
||||
data := `
|
||||
[A]
|
||||
B = "After"
|
||||
`
|
||||
|
||||
toml.Unmarshal([]byte(data), &d)
|
||||
fmt.Printf("toml v1: %#v\n", d)
|
||||
|
||||
// toml v1: main.doc{A:main.inner{B:"After"}}
|
||||
```
|
||||
|
||||
In this case, field `A` is of type `interface{}`, containing a `inner` struct.
|
||||
V1 sees that type and uses it when decoding the object.
|
||||
|
||||
When decoding an object into an `interface{}`, V2 instead disregards whatever
|
||||
value the `interface{}` may contain and replaces it with a
|
||||
`map[string]interface{}`. With the same data structure as above, here is what
|
||||
the result looks like:
|
||||
|
||||
```go
|
||||
toml.Unmarshal([]byte(data), &d)
|
||||
fmt.Printf("toml v2: %#v\n", d)
|
||||
|
||||
// toml v2: main.doc{A:map[string]interface {}{"B":"After"}}
|
||||
```
|
||||
|
||||
This is to match `encoding/json`'s behavior. There is no way to make the v2
|
||||
decoder behave like v1.
|
||||
|
||||
#### Values out of array bounds ignored
|
||||
|
||||
When decoding into an array, v1 returns an error when the number of elements
|
||||
contained in the doc is superior to the capacity of the array. For example:
|
||||
|
||||
```go
|
||||
type doc struct {
|
||||
A [2]string
|
||||
}
|
||||
d := doc{}
|
||||
err := toml.Unmarshal([]byte(`A = ["one", "two", "many"]`), &d)
|
||||
fmt.Println(err)
|
||||
|
||||
// (1, 1): unmarshal: TOML array length (3) exceeds destination array length (2)
|
||||
```
|
||||
|
||||
In the same situation, v2 ignores the last value:
|
||||
|
||||
```go
|
||||
err := toml.Unmarshal([]byte(`A = ["one", "two", "many"]`), &d)
|
||||
fmt.Println("err:", err, "d:", d)
|
||||
// err: <nil> d: {[one two]}
|
||||
```
|
||||
|
||||
This is to match `encoding/json`'s behavior. There is no way to make the v2
|
||||
decoder behave like v1.
|
||||
|
||||
#### Support for `toml.Unmarshaler` has been dropped
|
||||
|
||||
This method was not widely used, poorly defined, and added a lot of complexity.
|
||||
A similar effect can be achieved by implementing the `encoding.TextUnmarshaler`
|
||||
interface and use strings.
|
||||
|
||||
#### Support for `default` struct tag has been dropped
|
||||
|
||||
This feature adds complexity and a poorly defined API for an effect that can be
|
||||
accomplished outside of the library.
|
||||
|
||||
It does not seem like other format parsers in Go support that feature (the
|
||||
project referenced in the original ticket #202 has not been updated since 2017).
|
||||
Given that go-toml v2 should not touch values not in the document, the same
|
||||
effect can be achieved by pre-filling the struct with defaults (libraries like
|
||||
[go-defaults][go-defaults] can help). Also, string representation is not well
|
||||
defined for all types: it creates issues like #278.
|
||||
|
||||
The recommended replacement is pre-filling the struct before unmarshaling.
|
||||
|
||||
[go-defaults]: https://github.com/mcuadros/go-defaults
|
||||
|
||||
### Encoding / Marshal
|
||||
|
||||
#### Default struct fields order
|
||||
|
||||
V1 emits struct fields order alphabetically by default. V2 struct fields are
|
||||
emitted in order they are defined. For example:
|
||||
|
||||
```go
|
||||
type S struct {
|
||||
B string
|
||||
A string
|
||||
}
|
||||
|
||||
data := S{
|
||||
B: "B",
|
||||
A: "A",
|
||||
}
|
||||
|
||||
b, _ := tomlv1.Marshal(data)
|
||||
fmt.Println("v1:\n" + string(b))
|
||||
|
||||
b, _ = tomlv2.Marshal(data)
|
||||
fmt.Println("v2:\n" + string(b))
|
||||
|
||||
// Output:
|
||||
// v1:
|
||||
// A = "A"
|
||||
// B = "B"
|
||||
|
||||
// v2:
|
||||
// B = 'B'
|
||||
// A = 'A'
|
||||
```
|
||||
|
||||
There is no way to make v2 encoder behave like v1. A workaround could be to
|
||||
manually sort the fields alphabetically in the struct definition.
|
||||
|
||||
#### No indentation by default
|
||||
|
||||
V1 automatically indents content of tables by default. V2 does not. However the
|
||||
same behavior can be obtained using [`Encoder.SetIndentTables`][sit]. For example:
|
||||
|
||||
```go
|
||||
data := map[string]interface{}{
|
||||
"table": map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
}
|
||||
|
||||
b, _ := tomlv1.Marshal(data)
|
||||
fmt.Println("v1:\n" + string(b))
|
||||
|
||||
b, _ = tomlv2.Marshal(data)
|
||||
fmt.Println("v2:\n" + string(b))
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
enc := tomlv2.NewEncoder(&buf)
|
||||
enc.SetIndentTables(true)
|
||||
enc.Encode(data)
|
||||
fmt.Println("v2 Encoder:\n" + string(buf.Bytes()))
|
||||
|
||||
// Output:
|
||||
// v1:
|
||||
//
|
||||
// [table]
|
||||
// key = "value"
|
||||
//
|
||||
// v2:
|
||||
// [table]
|
||||
// key = 'value'
|
||||
//
|
||||
//
|
||||
// v2 Encoder:
|
||||
// [table]
|
||||
// key = 'value'
|
||||
```
|
||||
|
||||
[sit]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#Encoder.SetIndentTables
|
||||
|
||||
#### Keys and strings are single quoted
|
||||
|
||||
V1 always uses double quotes (`"`) around strings and keys that cannot be
|
||||
represented bare (unquoted). V2 uses single quotes instead by default (`'`),
|
||||
unless a character cannot be represented, then falls back to double quotes.
|
||||
|
||||
There is no way to make v2 encoder behave like v1.
|
||||
|
||||
#### `TextMarshaler` emits as a string, not TOML
|
||||
|
||||
Types that implement [`encoding.TextMarshaler`][tm] can emit arbitrary TOML in
|
||||
v1. The encoder would append the result to the output directly. In v2 the result
|
||||
is wrapped in a string. As a result, this interface cannot be implemented by the
|
||||
root object.
|
||||
|
||||
There is no way to make v2 encoder behave like v1.
|
||||
|
||||
[tm]: https://golang.org/pkg/encoding/#TextMarshaler
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2013 Thomas Pelletier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
The MIT License (MIT). Read [LICENSE](LICENSE).
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Use this section to tell people about which versions of your project are
|
||||
currently being supported with security updates.
|
||||
|
||||
| Version | Supported |
|
||||
| ---------- | ------------------ |
|
||||
| Latest 2.x | :white_check_mark: |
|
||||
| All 1.x | :x: |
|
||||
| All 0.x | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Email a vulnerability report to `security@pelletier.codes`. Make sure to include
|
||||
as many details as possible to reproduce the vulnerability. This is a
|
||||
side-project: I will try to get back to you as quickly as possible, time
|
||||
permitting in my personal life. Providing a working patch helps very much!
|
||||
@@ -0,0 +1,80 @@
|
||||
package benchmark_test
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var bench_inputs = []struct {
|
||||
name string
|
||||
jsonLen int
|
||||
}{
|
||||
// from https://gist.githubusercontent.com/feeeper/2197d6d734729625a037af1df14cf2aa/raw/2f22b120e476d897179be3c1e2483d18067aa7df/config.toml
|
||||
{"config", 806507},
|
||||
|
||||
// converted from https://github.com/miloyip/nativejson-benchmark
|
||||
{"canada", 2090234},
|
||||
{"citm_catalog", 479897},
|
||||
{"twitter", 428778},
|
||||
{"code", 1940472},
|
||||
|
||||
// converted from https://raw.githubusercontent.com/mailru/easyjson/master/benchmark/example.json
|
||||
{"example", 7779},
|
||||
}
|
||||
|
||||
func TestUnmarshalDatasetCode(t *testing.T) {
|
||||
for _, tc := range bench_inputs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
buf := fixture(t, tc.name)
|
||||
|
||||
var v interface{}
|
||||
require.NoError(t, toml.Unmarshal(buf, &v))
|
||||
|
||||
b, err := json.Marshal(v)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(b), tc.jsonLen)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUnmarshalDataset(b *testing.B) {
|
||||
for _, tc := range bench_inputs {
|
||||
b.Run(tc.name, func(b *testing.B) {
|
||||
buf := fixture(b, tc.name)
|
||||
b.SetBytes(int64(len(buf)))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
var v interface{}
|
||||
require.NoError(b, toml.Unmarshal(buf, &v))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// fixture returns the uncompressed contents of path.
|
||||
func fixture(tb testing.TB, path string) []byte {
|
||||
tb.Helper()
|
||||
|
||||
file := path + ".toml.gz"
|
||||
f, err := os.Open(filepath.Join("testdata", file))
|
||||
if os.IsNotExist(err) {
|
||||
tb.Skip("benchmark fixture not found:", file)
|
||||
}
|
||||
require.NoError(tb, err)
|
||||
defer f.Close()
|
||||
|
||||
gz, err := gzip.NewReader(f)
|
||||
require.NoError(tb, err)
|
||||
|
||||
buf, err := ioutil.ReadAll(gz)
|
||||
require.NoError(tb, err)
|
||||
return buf
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
################################################################################
|
||||
## Comment
|
||||
|
||||
# Speak your mind with the hash symbol. They go from the symbol to the end of
|
||||
# the line.
|
||||
|
||||
|
||||
################################################################################
|
||||
## Table
|
||||
|
||||
# Tables (also known as hash tables or dictionaries) are collections of
|
||||
# key/value pairs. They appear in square brackets on a line by themselves.
|
||||
|
||||
[table]
|
||||
|
||||
key = "value" # Yeah, you can do this.
|
||||
|
||||
# Nested tables are denoted by table names with dots in them. Name your tables
|
||||
# whatever crap you please, just don't use #, ., [ or ].
|
||||
|
||||
[table.subtable]
|
||||
|
||||
key = "another value"
|
||||
|
||||
# You don't need to specify all the super-tables if you don't want to. TOML
|
||||
# knows how to do it for you.
|
||||
|
||||
# [x] you
|
||||
# [x.y] don't
|
||||
# [x.y.z] need these
|
||||
[x.y.z.w] # for this to work
|
||||
|
||||
|
||||
################################################################################
|
||||
## Inline Table
|
||||
|
||||
# Inline tables provide a more compact syntax for expressing tables. They are
|
||||
# especially useful for grouped data that can otherwise quickly become verbose.
|
||||
# Inline tables are enclosed in curly braces `{` and `}`. No newlines are
|
||||
# allowed between the curly braces unless they are valid within a value.
|
||||
|
||||
[table.inline]
|
||||
|
||||
name = { first = "Tom", last = "Preston-Werner" }
|
||||
point = { x = 1, y = 2 }
|
||||
|
||||
|
||||
################################################################################
|
||||
## String
|
||||
|
||||
# There are four ways to express strings: basic, multi-line basic, literal, and
|
||||
# multi-line literal. All strings must contain only valid UTF-8 characters.
|
||||
|
||||
[string.basic]
|
||||
|
||||
basic = "I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF."
|
||||
|
||||
[string.multiline]
|
||||
|
||||
# The following strings are byte-for-byte equivalent:
|
||||
key1 = "One\nTwo"
|
||||
key2 = """One\nTwo"""
|
||||
key3 = """
|
||||
One
|
||||
Two"""
|
||||
|
||||
[string.multiline.continued]
|
||||
|
||||
# The following strings are byte-for-byte equivalent:
|
||||
key1 = "The quick brown fox jumps over the lazy dog."
|
||||
|
||||
key2 = """
|
||||
The quick brown \
|
||||
|
||||
|
||||
fox jumps over \
|
||||
the lazy dog."""
|
||||
|
||||
key3 = """\
|
||||
The quick brown \
|
||||
fox jumps over \
|
||||
the lazy dog.\
|
||||
"""
|
||||
|
||||
[string.literal]
|
||||
|
||||
# What you see is what you get.
|
||||
winpath = 'C:\Users\nodejs\templates'
|
||||
winpath2 = '\\ServerX\admin$\system32\'
|
||||
quoted = 'Tom "Dubs" Preston-Werner'
|
||||
regex = '<\i\c*\s*>'
|
||||
|
||||
|
||||
[string.literal.multiline]
|
||||
|
||||
regex2 = '''I [dw]on't need \d{2} apples'''
|
||||
lines = '''
|
||||
The first newline is
|
||||
trimmed in raw strings.
|
||||
All other whitespace
|
||||
is preserved.
|
||||
'''
|
||||
|
||||
|
||||
################################################################################
|
||||
## Integer
|
||||
|
||||
# Integers are whole numbers. Positive numbers may be prefixed with a plus sign.
|
||||
# Negative numbers are prefixed with a minus sign.
|
||||
|
||||
[integer]
|
||||
|
||||
key1 = +99
|
||||
key2 = 42
|
||||
key3 = 0
|
||||
key4 = -17
|
||||
|
||||
[integer.underscores]
|
||||
|
||||
# For large numbers, you may use underscores to enhance readability. Each
|
||||
# underscore must be surrounded by at least one digit.
|
||||
key1 = 1_000
|
||||
key2 = 5_349_221
|
||||
key3 = 1_2_3_4_5 # valid but inadvisable
|
||||
|
||||
|
||||
################################################################################
|
||||
## Float
|
||||
|
||||
# A float consists of an integer part (which may be prefixed with a plus or
|
||||
# minus sign) followed by a fractional part and/or an exponent part.
|
||||
|
||||
[float.fractional]
|
||||
|
||||
key1 = +1.0
|
||||
key2 = 3.1415
|
||||
key3 = -0.01
|
||||
|
||||
[float.exponent]
|
||||
|
||||
key1 = 5e+22
|
||||
key2 = 1e6
|
||||
key3 = -2E-2
|
||||
|
||||
[float.both]
|
||||
|
||||
key = 6.626e-34
|
||||
|
||||
[float.underscores]
|
||||
|
||||
key1 = 9_224_617.445_991_228_313
|
||||
key2 = 1e1_00
|
||||
|
||||
|
||||
################################################################################
|
||||
## Boolean
|
||||
|
||||
# Booleans are just the tokens you're used to. Always lowercase.
|
||||
|
||||
[boolean]
|
||||
|
||||
True = true
|
||||
False = false
|
||||
|
||||
|
||||
################################################################################
|
||||
## Datetime
|
||||
|
||||
# Datetimes are RFC 3339 dates.
|
||||
|
||||
[datetime]
|
||||
|
||||
key1 = 1979-05-27T07:32:00Z
|
||||
key2 = 1979-05-27T00:32:00-07:00
|
||||
key3 = 1979-05-27T00:32:00.999999-07:00
|
||||
|
||||
|
||||
################################################################################
|
||||
## Array
|
||||
|
||||
# Arrays are square brackets with other primitives inside. Whitespace is
|
||||
# ignored. Elements are separated by commas. Data types may not be mixed.
|
||||
|
||||
[array]
|
||||
|
||||
key1 = [ 1, 2, 3 ]
|
||||
key2 = [ "red", "yellow", "green" ]
|
||||
key3 = [ [ 1, 2 ], [3, 4, 5] ]
|
||||
key4 = [ [ 1, 2 ], ["a", "b", "c"] ] # this is ok
|
||||
|
||||
# Arrays can also be multiline. So in addition to ignoring whitespace, arrays
|
||||
# also ignore newlines between the brackets. Terminating commas are ok before
|
||||
# the closing bracket.
|
||||
|
||||
key5 = [
|
||||
1, 2, 3
|
||||
]
|
||||
key6 = [
|
||||
1,
|
||||
2, # this is ok
|
||||
]
|
||||
|
||||
|
||||
################################################################################
|
||||
## Array of Tables
|
||||
|
||||
# These can be expressed by using a table name in double brackets. Each table
|
||||
# with the same double bracketed name will be an element in the array. The
|
||||
# tables are inserted in the order encountered.
|
||||
|
||||
[[products]]
|
||||
|
||||
name = "Hammer"
|
||||
sku = 738594937
|
||||
|
||||
[[products]]
|
||||
|
||||
[[products]]
|
||||
|
||||
name = "Nail"
|
||||
sku = 284758393
|
||||
color = "gray"
|
||||
|
||||
|
||||
# You can create nested arrays of tables as well.
|
||||
|
||||
[[fruit]]
|
||||
name = "apple"
|
||||
|
||||
[fruit.physical]
|
||||
color = "red"
|
||||
shape = "round"
|
||||
|
||||
[[fruit.variety]]
|
||||
name = "red delicious"
|
||||
|
||||
[[fruit.variety]]
|
||||
name = "granny smith"
|
||||
|
||||
[[fruit]]
|
||||
name = "banana"
|
||||
|
||||
[[fruit.variety]]
|
||||
name = "plantain"
|
||||
@@ -0,0 +1,653 @@
|
||||
package benchmark_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUnmarshalSimple(t *testing.T) {
|
||||
doc := []byte(`A = "hello"`)
|
||||
d := struct {
|
||||
A string
|
||||
}{}
|
||||
|
||||
err := toml.Unmarshal(doc, &d)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUnmarshal(b *testing.B) {
|
||||
b.Run("SimpleDocument", func(b *testing.B) {
|
||||
doc := []byte(`A = "hello"`)
|
||||
|
||||
b.Run("struct", func(b *testing.B) {
|
||||
b.SetBytes(int64(len(doc)))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
d := struct {
|
||||
A string
|
||||
}{}
|
||||
|
||||
err := toml.Unmarshal(doc, &d)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("map", func(b *testing.B) {
|
||||
b.SetBytes(int64(len(doc)))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
d := map[string]interface{}{}
|
||||
err := toml.Unmarshal(doc, &d)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
b.Run("ReferenceFile", func(b *testing.B) {
|
||||
bytes, err := ioutil.ReadFile("benchmark.toml")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
b.Run("struct", func(b *testing.B) {
|
||||
b.SetBytes(int64(len(bytes)))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
d := benchmarkDoc{}
|
||||
err := toml.Unmarshal(bytes, &d)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("map", func(b *testing.B) {
|
||||
b.SetBytes(int64(len(bytes)))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
d := map[string]interface{}{}
|
||||
err := toml.Unmarshal(bytes, &d)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
b.Run("HugoFrontMatter", func(b *testing.B) {
|
||||
b.SetBytes(int64(len(hugoFrontMatterbytes)))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
d := map[string]interface{}{}
|
||||
err := toml.Unmarshal(hugoFrontMatterbytes, &d)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func marshal(v interface{}) ([]byte, error) {
|
||||
var b bytes.Buffer
|
||||
enc := toml.NewEncoder(&b)
|
||||
err := enc.Encode(v)
|
||||
return b.Bytes(), err
|
||||
}
|
||||
|
||||
func BenchmarkMarshal(b *testing.B) {
|
||||
b.Run("SimpleDocument", func(b *testing.B) {
|
||||
doc := []byte(`A = "hello"`)
|
||||
|
||||
b.Run("struct", func(b *testing.B) {
|
||||
d := struct {
|
||||
A string
|
||||
}{}
|
||||
|
||||
err := toml.Unmarshal(doc, &d)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
var out []byte
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
out, err = marshal(d)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
b.SetBytes(int64(len(out)))
|
||||
})
|
||||
|
||||
b.Run("map", func(b *testing.B) {
|
||||
d := map[string]interface{}{}
|
||||
err := toml.Unmarshal(doc, &d)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
var out []byte
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
out, err = marshal(d)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
b.SetBytes(int64(len(out)))
|
||||
})
|
||||
})
|
||||
|
||||
b.Run("ReferenceFile", func(b *testing.B) {
|
||||
bytes, err := ioutil.ReadFile("benchmark.toml")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
b.Run("struct", func(b *testing.B) {
|
||||
d := benchmarkDoc{}
|
||||
err := toml.Unmarshal(bytes, &d)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
var out []byte
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
out, err = marshal(d)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
b.SetBytes(int64(len(out)))
|
||||
})
|
||||
|
||||
b.Run("map", func(b *testing.B) {
|
||||
d := map[string]interface{}{}
|
||||
err := toml.Unmarshal(bytes, &d)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
var out []byte
|
||||
for i := 0; i < b.N; i++ {
|
||||
out, err = marshal(d)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
b.SetBytes(int64(len(out)))
|
||||
})
|
||||
})
|
||||
|
||||
b.Run("HugoFrontMatter", func(b *testing.B) {
|
||||
d := map[string]interface{}{}
|
||||
err := toml.Unmarshal(hugoFrontMatterbytes, &d)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
var out []byte
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
out, err = marshal(d)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
b.SetBytes(int64(len(out)))
|
||||
})
|
||||
}
|
||||
|
||||
type benchmarkDoc struct {
|
||||
Table struct {
|
||||
Key string
|
||||
Subtable struct {
|
||||
Key string
|
||||
}
|
||||
Inline struct {
|
||||
Name struct {
|
||||
First string
|
||||
Last string
|
||||
}
|
||||
Point struct {
|
||||
X int64
|
||||
Y int64
|
||||
}
|
||||
}
|
||||
}
|
||||
String struct {
|
||||
Basic struct {
|
||||
Basic string
|
||||
}
|
||||
Multiline struct {
|
||||
Key1 string
|
||||
Key2 string
|
||||
Key3 string
|
||||
Continued struct {
|
||||
Key1 string
|
||||
Key2 string
|
||||
Key3 string
|
||||
}
|
||||
}
|
||||
Literal struct {
|
||||
Winpath string
|
||||
Winpath2 string
|
||||
Quoted string
|
||||
Regex string
|
||||
Multiline struct {
|
||||
Regex2 string
|
||||
Lines string
|
||||
}
|
||||
}
|
||||
}
|
||||
Integer struct {
|
||||
Key1 int64
|
||||
Key2 int64
|
||||
Key3 int64
|
||||
Key4 int64
|
||||
Underscores struct {
|
||||
Key1 int64
|
||||
Key2 int64
|
||||
Key3 int64
|
||||
}
|
||||
}
|
||||
Float struct {
|
||||
Fractional struct {
|
||||
Key1 float64
|
||||
Key2 float64
|
||||
Key3 float64
|
||||
}
|
||||
Exponent struct {
|
||||
Key1 float64
|
||||
Key2 float64
|
||||
Key3 float64
|
||||
}
|
||||
Both struct {
|
||||
Key float64
|
||||
}
|
||||
Underscores struct {
|
||||
Key1 float64
|
||||
Key2 float64
|
||||
}
|
||||
}
|
||||
Boolean struct {
|
||||
True bool
|
||||
False bool
|
||||
}
|
||||
Datetime struct {
|
||||
Key1 time.Time
|
||||
Key2 time.Time
|
||||
Key3 time.Time
|
||||
}
|
||||
Array struct {
|
||||
Key1 []int64
|
||||
Key2 []string
|
||||
Key3 [][]int64
|
||||
Key4 []interface{}
|
||||
Key5 []int64
|
||||
Key6 []int64
|
||||
}
|
||||
Products []struct {
|
||||
Name string
|
||||
Sku int64
|
||||
Color string
|
||||
}
|
||||
Fruit []struct {
|
||||
Name string
|
||||
Physical struct {
|
||||
Color string
|
||||
Shape string
|
||||
}
|
||||
Variety []struct {
|
||||
Name string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalReferenceFile(t *testing.T) {
|
||||
bytes, err := ioutil.ReadFile("benchmark.toml")
|
||||
require.NoError(t, err)
|
||||
d := benchmarkDoc{}
|
||||
err = toml.Unmarshal(bytes, &d)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := benchmarkDoc{
|
||||
Table: struct {
|
||||
Key string
|
||||
Subtable struct{ Key string }
|
||||
Inline struct {
|
||||
Name struct {
|
||||
First string
|
||||
Last string
|
||||
}
|
||||
Point struct {
|
||||
X int64
|
||||
Y int64
|
||||
}
|
||||
}
|
||||
}{
|
||||
Key: "value",
|
||||
Subtable: struct{ Key string }{
|
||||
Key: "another value",
|
||||
},
|
||||
// note: x.y.z.w is purposefully missing
|
||||
Inline: struct {
|
||||
Name struct {
|
||||
First string
|
||||
Last string
|
||||
}
|
||||
Point struct {
|
||||
X int64
|
||||
Y int64
|
||||
}
|
||||
}{
|
||||
Name: struct {
|
||||
First string
|
||||
Last string
|
||||
}{
|
||||
First: "Tom",
|
||||
Last: "Preston-Werner",
|
||||
},
|
||||
Point: struct {
|
||||
X int64
|
||||
Y int64
|
||||
}{
|
||||
X: 1,
|
||||
Y: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
String: struct {
|
||||
Basic struct{ Basic string }
|
||||
Multiline struct {
|
||||
Key1 string
|
||||
Key2 string
|
||||
Key3 string
|
||||
Continued struct {
|
||||
Key1 string
|
||||
Key2 string
|
||||
Key3 string
|
||||
}
|
||||
}
|
||||
Literal struct {
|
||||
Winpath string
|
||||
Winpath2 string
|
||||
Quoted string
|
||||
Regex string
|
||||
Multiline struct {
|
||||
Regex2 string
|
||||
Lines string
|
||||
}
|
||||
}
|
||||
}{
|
||||
Basic: struct{ Basic string }{
|
||||
Basic: "I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF.",
|
||||
},
|
||||
Multiline: struct {
|
||||
Key1 string
|
||||
Key2 string
|
||||
Key3 string
|
||||
Continued struct {
|
||||
Key1 string
|
||||
Key2 string
|
||||
Key3 string
|
||||
}
|
||||
}{
|
||||
Key1: "One\nTwo",
|
||||
Key2: "One\nTwo",
|
||||
Key3: "One\nTwo",
|
||||
|
||||
Continued: struct {
|
||||
Key1 string
|
||||
Key2 string
|
||||
Key3 string
|
||||
}{
|
||||
Key1: `The quick brown fox jumps over the lazy dog.`,
|
||||
Key2: `The quick brown fox jumps over the lazy dog.`,
|
||||
Key3: `The quick brown fox jumps over the lazy dog.`,
|
||||
},
|
||||
},
|
||||
Literal: struct {
|
||||
Winpath string
|
||||
Winpath2 string
|
||||
Quoted string
|
||||
Regex string
|
||||
Multiline struct {
|
||||
Regex2 string
|
||||
Lines string
|
||||
}
|
||||
}{
|
||||
Winpath: `C:\Users\nodejs\templates`,
|
||||
Winpath2: `\\ServerX\admin$\system32\`,
|
||||
Quoted: `Tom "Dubs" Preston-Werner`,
|
||||
Regex: `<\i\c*\s*>`,
|
||||
|
||||
Multiline: struct {
|
||||
Regex2 string
|
||||
Lines string
|
||||
}{
|
||||
Regex2: `I [dw]on't need \d{2} apples`,
|
||||
Lines: `The first newline is
|
||||
trimmed in raw strings.
|
||||
All other whitespace
|
||||
is preserved.
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
Integer: struct {
|
||||
Key1 int64
|
||||
Key2 int64
|
||||
Key3 int64
|
||||
Key4 int64
|
||||
Underscores struct {
|
||||
Key1 int64
|
||||
Key2 int64
|
||||
Key3 int64
|
||||
}
|
||||
}{
|
||||
Key1: 99,
|
||||
Key2: 42,
|
||||
Key3: 0,
|
||||
Key4: -17,
|
||||
|
||||
Underscores: struct {
|
||||
Key1 int64
|
||||
Key2 int64
|
||||
Key3 int64
|
||||
}{
|
||||
Key1: 1000,
|
||||
Key2: 5349221,
|
||||
Key3: 12345,
|
||||
},
|
||||
},
|
||||
Float: struct {
|
||||
Fractional struct {
|
||||
Key1 float64
|
||||
Key2 float64
|
||||
Key3 float64
|
||||
}
|
||||
Exponent struct {
|
||||
Key1 float64
|
||||
Key2 float64
|
||||
Key3 float64
|
||||
}
|
||||
Both struct{ Key float64 }
|
||||
Underscores struct {
|
||||
Key1 float64
|
||||
Key2 float64
|
||||
}
|
||||
}{
|
||||
Fractional: struct {
|
||||
Key1 float64
|
||||
Key2 float64
|
||||
Key3 float64
|
||||
}{
|
||||
Key1: 1.0,
|
||||
Key2: 3.1415,
|
||||
Key3: -0.01,
|
||||
},
|
||||
Exponent: struct {
|
||||
Key1 float64
|
||||
Key2 float64
|
||||
Key3 float64
|
||||
}{
|
||||
Key1: 5e+22,
|
||||
Key2: 1e6,
|
||||
Key3: -2e-2,
|
||||
},
|
||||
Both: struct{ Key float64 }{
|
||||
Key: 6.626e-34,
|
||||
},
|
||||
Underscores: struct {
|
||||
Key1 float64
|
||||
Key2 float64
|
||||
}{
|
||||
Key1: 9224617.445991228313,
|
||||
Key2: 1e100,
|
||||
},
|
||||
},
|
||||
Boolean: struct {
|
||||
True bool
|
||||
False bool
|
||||
}{
|
||||
True: true,
|
||||
False: false,
|
||||
},
|
||||
Datetime: struct {
|
||||
Key1 time.Time
|
||||
Key2 time.Time
|
||||
Key3 time.Time
|
||||
}{
|
||||
Key1: time.Date(1979, 5, 27, 7, 32, 0, 0, time.UTC),
|
||||
Key2: time.Date(1979, 5, 27, 0, 32, 0, 0, time.FixedZone("", -7*3600)),
|
||||
Key3: time.Date(1979, 5, 27, 0, 32, 0, 999999000, time.FixedZone("", -7*3600)),
|
||||
},
|
||||
Array: struct {
|
||||
Key1 []int64
|
||||
Key2 []string
|
||||
Key3 [][]int64
|
||||
Key4 []interface{}
|
||||
Key5 []int64
|
||||
Key6 []int64
|
||||
}{
|
||||
Key1: []int64{1, 2, 3},
|
||||
Key2: []string{"red", "yellow", "green"},
|
||||
Key3: [][]int64{{1, 2}, {3, 4, 5}},
|
||||
Key4: []interface{}{
|
||||
[]interface{}{int64(1), int64(2)},
|
||||
[]interface{}{"a", "b", "c"},
|
||||
},
|
||||
Key5: []int64{1, 2, 3},
|
||||
Key6: []int64{1, 2},
|
||||
},
|
||||
Products: []struct {
|
||||
Name string
|
||||
Sku int64
|
||||
Color string
|
||||
}{
|
||||
{
|
||||
Name: "Hammer",
|
||||
Sku: 738594937,
|
||||
},
|
||||
{},
|
||||
{
|
||||
Name: "Nail",
|
||||
Sku: 284758393,
|
||||
Color: "gray",
|
||||
},
|
||||
},
|
||||
Fruit: []struct {
|
||||
Name string
|
||||
Physical struct {
|
||||
Color string
|
||||
Shape string
|
||||
}
|
||||
Variety []struct{ Name string }
|
||||
}{
|
||||
{
|
||||
Name: "apple",
|
||||
Physical: struct {
|
||||
Color string
|
||||
Shape string
|
||||
}{
|
||||
Color: "red",
|
||||
Shape: "round",
|
||||
},
|
||||
Variety: []struct{ Name string }{
|
||||
{Name: "red delicious"},
|
||||
{Name: "granny smith"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "banana",
|
||||
Variety: []struct{ Name string }{
|
||||
{Name: "plantain"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
require.Equal(t, expected, d)
|
||||
}
|
||||
|
||||
var hugoFrontMatterbytes = []byte(`
|
||||
categories = ["Development", "VIM"]
|
||||
date = "2012-04-06"
|
||||
description = "spf13-vim is a cross platform distribution of vim plugins and resources for Vim."
|
||||
slug = "spf13-vim-3-0-release-and-new-website"
|
||||
tags = [".vimrc", "plugins", "spf13-vim", "vim"]
|
||||
title = "spf13-vim 3.0 release and new website"
|
||||
include_toc = true
|
||||
show_comments = false
|
||||
|
||||
[[cascade]]
|
||||
background = "yosemite.jpg"
|
||||
[cascade._target]
|
||||
kind = "page"
|
||||
lang = "en"
|
||||
path = "/blog/**"
|
||||
|
||||
[[cascade]]
|
||||
background = "goldenbridge.jpg"
|
||||
[cascade._target]
|
||||
kind = "section"
|
||||
`)
|
||||
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,71 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var valid10Ascii = []byte("1234567890")
|
||||
var valid10Utf8 = []byte("日本語a")
|
||||
var valid1kUtf8 = bytes.Repeat([]byte("0123456789日本語日本語日本語日abcdefghijklmnopqrstuvwx"), 16)
|
||||
var valid1MUtf8 = bytes.Repeat(valid1kUtf8, 1024)
|
||||
var valid1kAscii = bytes.Repeat([]byte("012345678998jhjklasDJKLAAdjdfjsdklfjdslkabcdefghijklmnopqrstuvwx"), 16)
|
||||
var valid1MAscii = bytes.Repeat(valid1kAscii, 1024)
|
||||
|
||||
func BenchmarkScanComments(b *testing.B) {
|
||||
wrap := func(x []byte) []byte {
|
||||
return []byte("# " + string(x) + "\n")
|
||||
}
|
||||
|
||||
inputs := map[string][]byte{
|
||||
"10Valid": wrap(valid10Ascii),
|
||||
"1kValid": wrap(valid1kAscii),
|
||||
"1MValid": wrap(valid1MAscii),
|
||||
"10ValidUtf8": wrap(valid10Utf8),
|
||||
"1kValidUtf8": wrap(valid1kUtf8),
|
||||
"1MValidUtf8": wrap(valid1MUtf8),
|
||||
}
|
||||
|
||||
for name, input := range inputs {
|
||||
b.Run(name, func(b *testing.B) {
|
||||
b.SetBytes(int64(len(input)))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
scanComment(input)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkParseLiteralStringValid(b *testing.B) {
|
||||
wrap := func(x []byte) []byte {
|
||||
return []byte("'" + string(x) + "'")
|
||||
}
|
||||
|
||||
inputs := map[string][]byte{
|
||||
"10Valid": wrap(valid10Ascii),
|
||||
"1kValid": wrap(valid1kAscii),
|
||||
"1MValid": wrap(valid1MAscii),
|
||||
"10ValidUtf8": wrap(valid10Utf8),
|
||||
"1kValidUtf8": wrap(valid1kUtf8),
|
||||
"1MValidUtf8": wrap(valid1MUtf8),
|
||||
}
|
||||
|
||||
for name, input := range inputs {
|
||||
b.Run(name, func(b *testing.B) {
|
||||
p := parser{}
|
||||
b.SetBytes(int64(len(input)))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _, _, err := p.parseLiteralString(input)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
|
||||
stderr() {
|
||||
echo "$@" 1>&2
|
||||
}
|
||||
|
||||
usage() {
|
||||
b=$(basename "$0")
|
||||
echo $b: ERROR: "$@" 1>&2
|
||||
|
||||
cat 1>&2 <<EOF
|
||||
|
||||
DESCRIPTION
|
||||
|
||||
$(basename "$0") is the script to run continuous integration commands for
|
||||
go-toml on unix.
|
||||
|
||||
Requires Go and Git to be available in the PATH. Expects to be ran from the
|
||||
root of go-toml's Git repository.
|
||||
|
||||
USAGE
|
||||
|
||||
$b COMMAND [OPTIONS...]
|
||||
|
||||
COMMANDS
|
||||
|
||||
benchmark [OPTIONS...] [BRANCH]
|
||||
|
||||
Run benchmarks.
|
||||
|
||||
ARGUMENTS
|
||||
|
||||
BRANCH Optional. Defines which Git branch to use when running
|
||||
benchmarks.
|
||||
|
||||
OPTIONS
|
||||
|
||||
-d Compare benchmarks of HEAD with BRANCH using benchstats. In
|
||||
this form the BRANCH argument is required.
|
||||
|
||||
-a Compare benchmarks of HEAD against go-toml v1 and
|
||||
BurntSushi/toml.
|
||||
|
||||
-html When used with -a, emits the output as HTML, ready to be
|
||||
embedded in the README.
|
||||
|
||||
coverage [OPTIONS...] [BRANCH]
|
||||
|
||||
Generates code coverage.
|
||||
|
||||
ARGUMENTS
|
||||
|
||||
BRANCH Optional. Defines which Git branch to use when reporting
|
||||
coverage. Defaults to HEAD.
|
||||
|
||||
OPTIONS
|
||||
|
||||
-d Compare coverage of HEAD with the one of BRANCH. In this form,
|
||||
the BRANCH argument is required. Exit code is non-zero when
|
||||
coverage percentage decreased.
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
cover() {
|
||||
branch="${1}"
|
||||
dir="$(mktemp -d)"
|
||||
|
||||
stderr "Executing coverage for ${branch} at ${dir}"
|
||||
|
||||
if [ "${branch}" = "HEAD" ]; then
|
||||
cp -r . "${dir}/"
|
||||
else
|
||||
git worktree add "$dir" "$branch"
|
||||
fi
|
||||
|
||||
pushd "$dir"
|
||||
go test -covermode=atomic -coverprofile=coverage.out ./...
|
||||
go tool cover -func=coverage.out
|
||||
popd
|
||||
|
||||
if [ "${branch}" != "HEAD" ]; then
|
||||
git worktree remove --force "$dir"
|
||||
fi
|
||||
}
|
||||
|
||||
coverage() {
|
||||
case "$1" in
|
||||
-d)
|
||||
shift
|
||||
target="${1?Need to provide a target branch argument}"
|
||||
|
||||
output_dir="$(mktemp -d)"
|
||||
target_out="${output_dir}/target.txt"
|
||||
head_out="${output_dir}/head.txt"
|
||||
|
||||
cover "${target}" > "${target_out}"
|
||||
cover "HEAD" > "${head_out}"
|
||||
|
||||
cat "${target_out}"
|
||||
cat "${head_out}"
|
||||
|
||||
echo ""
|
||||
|
||||
target_pct="$(cat ${target_out} |sed -E 's/.*total.*\t([0-9.]+)%/\1/;t;d')"
|
||||
head_pct="$(cat ${head_out} |sed -E 's/.*total.*\t([0-9.]+)%/\1/;t;d')"
|
||||
echo "Results: ${target} ${target_pct}% HEAD ${head_pct}%"
|
||||
|
||||
delta_pct=$(echo "$head_pct - $target_pct" | bc -l)
|
||||
echo "Delta: ${delta_pct}"
|
||||
|
||||
if [[ $delta_pct = \-* ]]; then
|
||||
echo "Regression!";
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
cover "${1-HEAD}"
|
||||
}
|
||||
|
||||
bench() {
|
||||
branch="${1}"
|
||||
out="${2}"
|
||||
replace="${3}"
|
||||
dir="$(mktemp -d)"
|
||||
|
||||
stderr "Executing benchmark for ${branch} at ${dir}"
|
||||
|
||||
if [ "${branch}" = "HEAD" ]; then
|
||||
cp -r . "${dir}/"
|
||||
else
|
||||
git worktree add "$dir" "$branch"
|
||||
fi
|
||||
|
||||
pushd "$dir"
|
||||
|
||||
if [ "${replace}" != "" ]; then
|
||||
find ./benchmark/ -iname '*.go' -exec sed -i -E "s|github.com/pelletier/go-toml/v2|${replace}|g" {} \;
|
||||
go get "${replace}"
|
||||
fi
|
||||
|
||||
export GOMAXPROCS=2
|
||||
nice -n -19 taskset --cpu-list 0,1 go test '-bench=^Benchmark(Un)?[mM]arshal' -count=5 -run=Nothing ./... | tee "${out}"
|
||||
popd
|
||||
|
||||
if [ "${branch}" != "HEAD" ]; then
|
||||
git worktree remove --force "$dir"
|
||||
fi
|
||||
}
|
||||
|
||||
fmktemp() {
|
||||
if mktemp --version|grep GNU >/dev/null; then
|
||||
mktemp --suffix=-$1;
|
||||
else
|
||||
mktemp -t $1;
|
||||
fi
|
||||
}
|
||||
|
||||
benchstathtml() {
|
||||
python3 - $1 <<'EOF'
|
||||
import sys
|
||||
|
||||
lines = []
|
||||
stop = False
|
||||
|
||||
with open(sys.argv[1]) as f:
|
||||
for line in f.readlines():
|
||||
line = line.strip()
|
||||
if line == "":
|
||||
stop = True
|
||||
if not stop:
|
||||
lines.append(line.split(','))
|
||||
|
||||
results = []
|
||||
for line in reversed(lines[1:]):
|
||||
v2 = float(line[1])
|
||||
results.append([
|
||||
line[0].replace("-32", ""),
|
||||
"%.1fx" % (float(line[3])/v2), # v1
|
||||
"%.1fx" % (float(line[5])/v2), # bs
|
||||
])
|
||||
# move geomean to the end
|
||||
results.append(results[0])
|
||||
del results[0]
|
||||
|
||||
|
||||
def printtable(data):
|
||||
print("""
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
|
||||
</thead>
|
||||
<tbody>""")
|
||||
|
||||
for r in data:
|
||||
print(" <tr><td>{}</td><td>{}</td><td>{}</td></tr>".format(*r))
|
||||
|
||||
print(""" </tbody>
|
||||
</table>""")
|
||||
|
||||
|
||||
def match(x):
|
||||
return "ReferenceFile" in x[0] or "HugoFrontMatter" in x[0]
|
||||
|
||||
above = [x for x in results if match(x)]
|
||||
below = [x for x in results if not match(x)]
|
||||
|
||||
printtable(above)
|
||||
print("<details><summary>See more</summary>")
|
||||
print("""<p>The table above has the results of the most common use-cases. The table below
|
||||
contains the results of all benchmarks, including unrealistic ones. It is
|
||||
provided for completeness.</p>""")
|
||||
printtable(below)
|
||||
print('<p>This table can be generated with <code>./ci.sh benchmark -a -html</code>.</p>')
|
||||
print("</details>")
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
benchmark() {
|
||||
case "$1" in
|
||||
-d)
|
||||
shift
|
||||
target="${1?Need to provide a target branch argument}"
|
||||
|
||||
old=`fmktemp ${target}`
|
||||
bench "${target}" "${old}"
|
||||
|
||||
new=`fmktemp HEAD`
|
||||
bench HEAD "${new}"
|
||||
|
||||
benchstat "${old}" "${new}"
|
||||
return 0
|
||||
;;
|
||||
-a)
|
||||
shift
|
||||
|
||||
v2stats=`fmktemp go-toml-v2`
|
||||
bench HEAD "${v2stats}" "github.com/pelletier/go-toml/v2"
|
||||
v1stats=`fmktemp go-toml-v1`
|
||||
bench HEAD "${v1stats}" "github.com/pelletier/go-toml"
|
||||
bsstats=`fmktemp bs-toml`
|
||||
bench HEAD "${bsstats}" "github.com/BurntSushi/toml"
|
||||
|
||||
cp "${v2stats}" go-toml-v2.txt
|
||||
cp "${v1stats}" go-toml-v1.txt
|
||||
cp "${bsstats}" bs-toml.txt
|
||||
|
||||
if [ "$1" = "-html" ]; then
|
||||
tmpcsv=`fmktemp csv`
|
||||
benchstat -csv -geomean go-toml-v2.txt go-toml-v1.txt bs-toml.txt > $tmpcsv
|
||||
benchstathtml $tmpcsv
|
||||
else
|
||||
benchstat -geomean go-toml-v2.txt go-toml-v1.txt bs-toml.txt
|
||||
fi
|
||||
|
||||
rm -f go-toml-v2.txt go-toml-v1.txt bs-toml.txt
|
||||
return $?
|
||||
esac
|
||||
|
||||
bench "${1-HEAD}" `mktemp`
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
coverage) shift; coverage $@;;
|
||||
benchmark) shift; benchmark $@;;
|
||||
*) usage "bad argument $1";;
|
||||
esac
|
||||
@@ -0,0 +1,30 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/pelletier/go-toml/v2/testsuite"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
if flag.NArg() != 0 {
|
||||
flag.Usage()
|
||||
}
|
||||
|
||||
err := testsuite.DecodeStdin()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func usage() {
|
||||
log.Printf("Usage: %s < toml-file\n", path.Base(os.Args[0]))
|
||||
flag.PrintDefaults()
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
// tomltestgen retrieves a given version of the language-agnostic TOML test suite in
|
||||
// https://github.com/BurntSushi/toml-test and generates go-toml unit tests.
|
||||
//
|
||||
// Within the go-toml package, run `go generate`. Otherwise, use:
|
||||
//
|
||||
// go run github.com/pelletier/go-toml/cmd/tomltestgen -o toml_testgen_test.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
type invalid struct {
|
||||
Name string
|
||||
Input string
|
||||
}
|
||||
|
||||
type valid struct {
|
||||
Name string
|
||||
Input string
|
||||
JsonRef string
|
||||
}
|
||||
|
||||
type testsCollection struct {
|
||||
Ref string
|
||||
Timestamp string
|
||||
Invalid []invalid
|
||||
Valid []valid
|
||||
Count int
|
||||
}
|
||||
|
||||
const srcTemplate = "// Generated by tomltestgen for toml-test ref {{.Ref}} on {{.Timestamp}}\n" +
|
||||
"package toml_test\n" +
|
||||
" import (\n" +
|
||||
" \"testing\"\n" +
|
||||
")\n" +
|
||||
|
||||
"{{range .Invalid}}\n" +
|
||||
"func TestTOMLTest_Invalid_{{.Name}}(t *testing.T) {\n" +
|
||||
" input := {{.Input|gostr}}\n" +
|
||||
" testgenInvalid(t, input)\n" +
|
||||
"}\n" +
|
||||
"{{end}}\n" +
|
||||
"\n" +
|
||||
"{{range .Valid}}\n" +
|
||||
"func TestTOMLTest_Valid_{{.Name}}(t *testing.T) {\n" +
|
||||
" input := {{.Input|gostr}}\n" +
|
||||
" jsonRef := {{.JsonRef|gostr}}\n" +
|
||||
" testgenValid(t, input, jsonRef)\n" +
|
||||
"}\n" +
|
||||
"{{end}}\n"
|
||||
|
||||
func downloadTmpFile(url string) string {
|
||||
log.Println("starting to download file from", url)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
tmpfile, err := ioutil.TempFile("", "toml-test-*.zip")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer tmpfile.Close()
|
||||
|
||||
copiedLen, err := io.Copy(tmpfile, resp.Body)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if resp.ContentLength > 0 && copiedLen != resp.ContentLength {
|
||||
panic(fmt.Errorf("copied %d bytes, request body had %d", copiedLen, resp.ContentLength))
|
||||
}
|
||||
return tmpfile.Name()
|
||||
}
|
||||
|
||||
func kebabToCamel(kebab string) string {
|
||||
camel := ""
|
||||
nextUpper := true
|
||||
for _, c := range kebab {
|
||||
if nextUpper {
|
||||
camel += strings.ToUpper(string(c))
|
||||
nextUpper = false
|
||||
} else if c == '-' {
|
||||
nextUpper = true
|
||||
} else if c == '/' {
|
||||
nextUpper = true
|
||||
camel += "_"
|
||||
} else {
|
||||
camel += string(c)
|
||||
}
|
||||
}
|
||||
return camel
|
||||
}
|
||||
|
||||
func readFileFromZip(f *zip.File) string {
|
||||
reader, err := f.Open()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer reader.Close()
|
||||
bytes, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
func templateGoStr(input string) string {
|
||||
return strconv.Quote(input)
|
||||
}
|
||||
|
||||
var (
|
||||
ref = flag.String("r", "master", "git reference")
|
||||
out = flag.String("o", "", "output file")
|
||||
)
|
||||
|
||||
func usage() {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "usage: tomltestgen [flags]\n")
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
|
||||
url := "https://codeload.github.com/BurntSushi/toml-test/zip/" + *ref
|
||||
resultFile := downloadTmpFile(url)
|
||||
defer os.Remove(resultFile)
|
||||
log.Println("file written to", resultFile)
|
||||
|
||||
zipReader, err := zip.OpenReader(resultFile)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer zipReader.Close()
|
||||
|
||||
collection := testsCollection{
|
||||
Ref: *ref,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
zipFilesMap := map[string]*zip.File{}
|
||||
|
||||
for _, f := range zipReader.File {
|
||||
zipFilesMap[f.Name] = f
|
||||
}
|
||||
|
||||
testFileRegexp := regexp.MustCompile(`([^/]+/tests/(valid|invalid)/(.+))\.(toml)`)
|
||||
for _, f := range zipReader.File {
|
||||
groups := testFileRegexp.FindStringSubmatch(f.Name)
|
||||
if len(groups) > 0 {
|
||||
name := kebabToCamel(groups[3])
|
||||
testType := groups[2]
|
||||
|
||||
log.Printf("> [%s] %s\n", testType, name)
|
||||
|
||||
tomlContent := readFileFromZip(f)
|
||||
|
||||
switch testType {
|
||||
case "invalid":
|
||||
collection.Invalid = append(collection.Invalid, invalid{
|
||||
Name: name,
|
||||
Input: tomlContent,
|
||||
})
|
||||
collection.Count++
|
||||
case "valid":
|
||||
baseFilePath := groups[1]
|
||||
jsonFilePath := baseFilePath + ".json"
|
||||
jsonContent := readFileFromZip(zipFilesMap[jsonFilePath])
|
||||
|
||||
collection.Valid = append(collection.Valid, valid{
|
||||
Name: name,
|
||||
Input: tomlContent,
|
||||
JsonRef: jsonContent,
|
||||
})
|
||||
collection.Count++
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown test type: %s", testType))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Collected %d tests from toml-test\n", collection.Count)
|
||||
|
||||
funcMap := template.FuncMap{
|
||||
"gostr": templateGoStr,
|
||||
}
|
||||
t := template.Must(template.New("src").Funcs(funcMap).Parse(srcTemplate))
|
||||
buf := new(bytes.Buffer)
|
||||
err = t.Execute(buf, collection)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
outputBytes, err := format.Source(buf.Bytes())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if *out == "" {
|
||||
fmt.Println(string(outputBytes))
|
||||
return
|
||||
}
|
||||
|
||||
err = os.WriteFile(*out, outputBytes, 0644)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,496 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func parseInteger(b []byte) (int64, error) {
|
||||
if len(b) > 2 && b[0] == '0' {
|
||||
switch b[1] {
|
||||
case 'x':
|
||||
return parseIntHex(b)
|
||||
case 'b':
|
||||
return parseIntBin(b)
|
||||
case 'o':
|
||||
return parseIntOct(b)
|
||||
default:
|
||||
panic(fmt.Errorf("invalid base '%c', should have been checked by scanIntOrFloat", b[1]))
|
||||
}
|
||||
}
|
||||
|
||||
return parseIntDec(b)
|
||||
}
|
||||
|
||||
func parseLocalDate(b []byte) (LocalDate, error) {
|
||||
// full-date = date-fullyear "-" date-month "-" date-mday
|
||||
// date-fullyear = 4DIGIT
|
||||
// date-month = 2DIGIT ; 01-12
|
||||
// date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on month/year
|
||||
var date LocalDate
|
||||
|
||||
if len(b) != 10 || b[4] != '-' || b[7] != '-' {
|
||||
return date, newDecodeError(b, "dates are expected to have the format YYYY-MM-DD")
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
date.Year, err = parseDecimalDigits(b[0:4])
|
||||
if err != nil {
|
||||
return LocalDate{}, err
|
||||
}
|
||||
|
||||
date.Month, err = parseDecimalDigits(b[5:7])
|
||||
if err != nil {
|
||||
return LocalDate{}, err
|
||||
}
|
||||
|
||||
date.Day, err = parseDecimalDigits(b[8:10])
|
||||
if err != nil {
|
||||
return LocalDate{}, err
|
||||
}
|
||||
|
||||
if !isValidDate(date.Year, date.Month, date.Day) {
|
||||
return LocalDate{}, newDecodeError(b, "impossible date")
|
||||
}
|
||||
|
||||
return date, nil
|
||||
}
|
||||
|
||||
func parseDecimalDigits(b []byte) (int, error) {
|
||||
v := 0
|
||||
|
||||
for i, c := range b {
|
||||
if c < '0' || c > '9' {
|
||||
return 0, newDecodeError(b[i:i+1], "expected digit (0-9)")
|
||||
}
|
||||
v *= 10
|
||||
v += int(c - '0')
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func parseDateTime(b []byte) (time.Time, error) {
|
||||
// offset-date-time = full-date time-delim full-time
|
||||
// full-time = partial-time time-offset
|
||||
// time-offset = "Z" / time-numoffset
|
||||
// time-numoffset = ( "+" / "-" ) time-hour ":" time-minute
|
||||
|
||||
dt, b, err := parseLocalDateTime(b)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
|
||||
var zone *time.Location
|
||||
|
||||
if len(b) == 0 {
|
||||
// parser should have checked that when assigning the date time node
|
||||
panic("date time should have a timezone")
|
||||
}
|
||||
|
||||
if b[0] == 'Z' || b[0] == 'z' {
|
||||
b = b[1:]
|
||||
zone = time.UTC
|
||||
} else {
|
||||
const dateTimeByteLen = 6
|
||||
if len(b) != dateTimeByteLen {
|
||||
return time.Time{}, newDecodeError(b, "invalid date-time timezone")
|
||||
}
|
||||
direction := 1
|
||||
if b[0] == '-' {
|
||||
direction = -1
|
||||
}
|
||||
|
||||
hours := digitsToInt(b[1:3])
|
||||
minutes := digitsToInt(b[4:6])
|
||||
seconds := direction * (hours*3600 + minutes*60)
|
||||
zone = time.FixedZone("", seconds)
|
||||
b = b[dateTimeByteLen:]
|
||||
}
|
||||
|
||||
if len(b) > 0 {
|
||||
return time.Time{}, newDecodeError(b, "extra bytes at the end of the timezone")
|
||||
}
|
||||
|
||||
t := time.Date(
|
||||
dt.Year,
|
||||
time.Month(dt.Month),
|
||||
dt.Day,
|
||||
dt.Hour,
|
||||
dt.Minute,
|
||||
dt.Second,
|
||||
dt.Nanosecond,
|
||||
zone)
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func parseLocalDateTime(b []byte) (LocalDateTime, []byte, error) {
|
||||
var dt LocalDateTime
|
||||
|
||||
const localDateTimeByteMinLen = 11
|
||||
if len(b) < localDateTimeByteMinLen {
|
||||
return dt, nil, newDecodeError(b, "local datetimes are expected to have the format YYYY-MM-DDTHH:MM:SS[.NNNNNNNNN]")
|
||||
}
|
||||
|
||||
date, err := parseLocalDate(b[:10])
|
||||
if err != nil {
|
||||
return dt, nil, err
|
||||
}
|
||||
dt.LocalDate = date
|
||||
|
||||
sep := b[10]
|
||||
if sep != 'T' && sep != ' ' && sep != 't' {
|
||||
return dt, nil, newDecodeError(b[10:11], "datetime separator is expected to be T or a space")
|
||||
}
|
||||
|
||||
t, rest, err := parseLocalTime(b[11:])
|
||||
if err != nil {
|
||||
return dt, nil, err
|
||||
}
|
||||
dt.LocalTime = t
|
||||
|
||||
return dt, rest, nil
|
||||
}
|
||||
|
||||
// parseLocalTime is a bit different because it also returns the remaining
|
||||
// []byte that is didn't need. This is to allow parseDateTime to parse those
|
||||
// remaining bytes as a timezone.
|
||||
func parseLocalTime(b []byte) (LocalTime, []byte, error) {
|
||||
var (
|
||||
nspow = [10]int{0, 1e8, 1e7, 1e6, 1e5, 1e4, 1e3, 1e2, 1e1, 1e0}
|
||||
t LocalTime
|
||||
)
|
||||
|
||||
// check if b matches to have expected format HH:MM:SS[.NNNNNN]
|
||||
const localTimeByteLen = 8
|
||||
if len(b) < localTimeByteLen {
|
||||
return t, nil, newDecodeError(b, "times are expected to have the format HH:MM:SS[.NNNNNN]")
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
t.Hour, err = parseDecimalDigits(b[0:2])
|
||||
if err != nil {
|
||||
return t, nil, err
|
||||
}
|
||||
|
||||
if t.Hour > 23 {
|
||||
return t, nil, newDecodeError(b[0:2], "hour cannot be greater 23")
|
||||
}
|
||||
if b[2] != ':' {
|
||||
return t, nil, newDecodeError(b[2:3], "expecting colon between hours and minutes")
|
||||
}
|
||||
|
||||
t.Minute, err = parseDecimalDigits(b[3:5])
|
||||
if err != nil {
|
||||
return t, nil, err
|
||||
}
|
||||
if t.Minute > 59 {
|
||||
return t, nil, newDecodeError(b[3:5], "minutes cannot be greater 59")
|
||||
}
|
||||
if b[5] != ':' {
|
||||
return t, nil, newDecodeError(b[5:6], "expecting colon between minutes and seconds")
|
||||
}
|
||||
|
||||
t.Second, err = parseDecimalDigits(b[6:8])
|
||||
if err != nil {
|
||||
return t, nil, err
|
||||
}
|
||||
|
||||
if t.Second > 59 {
|
||||
return t, nil, newDecodeError(b[3:5], "seconds cannot be greater 59")
|
||||
}
|
||||
|
||||
const minLengthWithFrac = 9
|
||||
if len(b) >= minLengthWithFrac && b[minLengthWithFrac-1] == '.' {
|
||||
frac := 0
|
||||
digits := 0
|
||||
|
||||
for i, c := range b[minLengthWithFrac:] {
|
||||
if !isDigit(c) {
|
||||
if i == 0 {
|
||||
return t, nil, newDecodeError(b[i:i+1], "need at least one digit after fraction point")
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
const maxFracPrecision = 9
|
||||
if i >= maxFracPrecision {
|
||||
return t, nil, newDecodeError(b[i:i+1], "maximum precision for date time is nanosecond")
|
||||
}
|
||||
|
||||
frac *= 10
|
||||
frac += int(c - '0')
|
||||
digits++
|
||||
}
|
||||
|
||||
if digits == 0 {
|
||||
return t, nil, newDecodeError(b[minLengthWithFrac-1:minLengthWithFrac], "nanoseconds need at least one digit")
|
||||
}
|
||||
|
||||
t.Nanosecond = frac * nspow[digits]
|
||||
t.Precision = digits
|
||||
|
||||
return t, b[9+digits:], nil
|
||||
}
|
||||
|
||||
return t, b[8:], nil
|
||||
}
|
||||
|
||||
//nolint:cyclop
|
||||
func parseFloat(b []byte) (float64, error) {
|
||||
if len(b) == 4 && (b[0] == '+' || b[0] == '-') && b[1] == 'n' && b[2] == 'a' && b[3] == 'n' {
|
||||
return math.NaN(), nil
|
||||
}
|
||||
|
||||
cleaned, err := checkAndRemoveUnderscoresFloats(b)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if cleaned[0] == '.' {
|
||||
return 0, newDecodeError(b, "float cannot start with a dot")
|
||||
}
|
||||
|
||||
if cleaned[len(cleaned)-1] == '.' {
|
||||
return 0, newDecodeError(b, "float cannot end with a dot")
|
||||
}
|
||||
|
||||
dotAlreadySeen := false
|
||||
for i, c := range cleaned {
|
||||
if c == '.' {
|
||||
if dotAlreadySeen {
|
||||
return 0, newDecodeError(b[i:i+1], "float can have at most one decimal point")
|
||||
}
|
||||
if !isDigit(cleaned[i-1]) {
|
||||
return 0, newDecodeError(b[i-1:i+1], "float decimal point must be preceded by a digit")
|
||||
}
|
||||
if !isDigit(cleaned[i+1]) {
|
||||
return 0, newDecodeError(b[i:i+2], "float decimal point must be followed by a digit")
|
||||
}
|
||||
dotAlreadySeen = true
|
||||
}
|
||||
}
|
||||
|
||||
start := 0
|
||||
if b[0] == '+' || b[0] == '-' {
|
||||
start = 1
|
||||
}
|
||||
if b[start] == '0' && isDigit(b[start+1]) {
|
||||
return 0, newDecodeError(b, "float integer part cannot have leading zeroes")
|
||||
}
|
||||
|
||||
f, err := strconv.ParseFloat(string(cleaned), 64)
|
||||
if err != nil {
|
||||
return 0, newDecodeError(b, "unable to parse float: %w", err)
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func parseIntHex(b []byte) (int64, error) {
|
||||
cleaned, err := checkAndRemoveUnderscoresIntegers(b[2:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
i, err := strconv.ParseInt(string(cleaned), 16, 64)
|
||||
if err != nil {
|
||||
return 0, newDecodeError(b, "couldn't parse hexadecimal number: %w", err)
|
||||
}
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func parseIntOct(b []byte) (int64, error) {
|
||||
cleaned, err := checkAndRemoveUnderscoresIntegers(b[2:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
i, err := strconv.ParseInt(string(cleaned), 8, 64)
|
||||
if err != nil {
|
||||
return 0, newDecodeError(b, "couldn't parse octal number: %w", err)
|
||||
}
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func parseIntBin(b []byte) (int64, error) {
|
||||
cleaned, err := checkAndRemoveUnderscoresIntegers(b[2:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
i, err := strconv.ParseInt(string(cleaned), 2, 64)
|
||||
if err != nil {
|
||||
return 0, newDecodeError(b, "couldn't parse binary number: %w", err)
|
||||
}
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func isSign(b byte) bool {
|
||||
return b == '+' || b == '-'
|
||||
}
|
||||
|
||||
func parseIntDec(b []byte) (int64, error) {
|
||||
cleaned, err := checkAndRemoveUnderscoresIntegers(b)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
startIdx := 0
|
||||
|
||||
if isSign(cleaned[0]) {
|
||||
startIdx++
|
||||
}
|
||||
|
||||
if len(cleaned) > startIdx+1 && cleaned[startIdx] == '0' {
|
||||
return 0, newDecodeError(b, "leading zero not allowed on decimal number")
|
||||
}
|
||||
|
||||
i, err := strconv.ParseInt(string(cleaned), 10, 64)
|
||||
if err != nil {
|
||||
return 0, newDecodeError(b, "couldn't parse decimal number: %w", err)
|
||||
}
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func checkAndRemoveUnderscoresIntegers(b []byte) ([]byte, error) {
|
||||
if b[0] == '_' {
|
||||
return nil, newDecodeError(b[0:1], "number cannot start with underscore")
|
||||
}
|
||||
|
||||
if b[len(b)-1] == '_' {
|
||||
return nil, newDecodeError(b[len(b)-1:], "number cannot end with underscore")
|
||||
}
|
||||
|
||||
// fast path
|
||||
i := 0
|
||||
for ; i < len(b); i++ {
|
||||
if b[i] == '_' {
|
||||
break
|
||||
}
|
||||
}
|
||||
if i == len(b) {
|
||||
return b, nil
|
||||
}
|
||||
|
||||
before := false
|
||||
cleaned := make([]byte, i, len(b))
|
||||
copy(cleaned, b)
|
||||
|
||||
for i++; i < len(b); i++ {
|
||||
c := b[i]
|
||||
if c == '_' {
|
||||
if !before {
|
||||
return nil, newDecodeError(b[i-1:i+1], "number must have at least one digit between underscores")
|
||||
}
|
||||
before = false
|
||||
} else {
|
||||
before = true
|
||||
cleaned = append(cleaned, c)
|
||||
}
|
||||
}
|
||||
|
||||
return cleaned, nil
|
||||
}
|
||||
|
||||
func checkAndRemoveUnderscoresFloats(b []byte) ([]byte, error) {
|
||||
if b[0] == '_' {
|
||||
return nil, newDecodeError(b[0:1], "number cannot start with underscore")
|
||||
}
|
||||
|
||||
if b[len(b)-1] == '_' {
|
||||
return nil, newDecodeError(b[len(b)-1:], "number cannot end with underscore")
|
||||
}
|
||||
|
||||
// fast path
|
||||
i := 0
|
||||
for ; i < len(b); i++ {
|
||||
if b[i] == '_' {
|
||||
break
|
||||
}
|
||||
}
|
||||
if i == len(b) {
|
||||
return b, nil
|
||||
}
|
||||
|
||||
before := false
|
||||
cleaned := make([]byte, 0, len(b))
|
||||
|
||||
for i := 0; i < len(b); i++ {
|
||||
c := b[i]
|
||||
|
||||
switch c {
|
||||
case '_':
|
||||
if !before {
|
||||
return nil, newDecodeError(b[i-1:i+1], "number must have at least one digit between underscores")
|
||||
}
|
||||
if i < len(b)-1 && (b[i+1] == 'e' || b[i+1] == 'E') {
|
||||
return nil, newDecodeError(b[i+1:i+2], "cannot have underscore before exponent")
|
||||
}
|
||||
before = false
|
||||
case 'e', 'E':
|
||||
if i < len(b)-1 && b[i+1] == '_' {
|
||||
return nil, newDecodeError(b[i+1:i+2], "cannot have underscore after exponent")
|
||||
}
|
||||
cleaned = append(cleaned, c)
|
||||
case '.':
|
||||
if i < len(b)-1 && b[i+1] == '_' {
|
||||
return nil, newDecodeError(b[i+1:i+2], "cannot have underscore after decimal point")
|
||||
}
|
||||
if i > 0 && b[i-1] == '_' {
|
||||
return nil, newDecodeError(b[i-1:i], "cannot have underscore before decimal point")
|
||||
}
|
||||
cleaned = append(cleaned, c)
|
||||
default:
|
||||
before = true
|
||||
cleaned = append(cleaned, c)
|
||||
}
|
||||
}
|
||||
|
||||
return cleaned, nil
|
||||
}
|
||||
|
||||
// isValidDate checks if a provided date is a date that exists.
|
||||
func isValidDate(year int, month int, day int) bool {
|
||||
return day <= daysIn(month, year)
|
||||
}
|
||||
|
||||
// daysBefore[m] counts the number of days in a non-leap year
|
||||
// before month m begins. There is an entry for m=12, counting
|
||||
// the number of days before January of next year (365).
|
||||
var daysBefore = [...]int32{
|
||||
0,
|
||||
31,
|
||||
31 + 28,
|
||||
31 + 28 + 31,
|
||||
31 + 28 + 31 + 30,
|
||||
31 + 28 + 31 + 30 + 31,
|
||||
31 + 28 + 31 + 30 + 31 + 30,
|
||||
31 + 28 + 31 + 30 + 31 + 30 + 31,
|
||||
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31,
|
||||
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30,
|
||||
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31,
|
||||
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30,
|
||||
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30 + 31,
|
||||
}
|
||||
|
||||
func daysIn(m int, year int) int {
|
||||
if m == 2 && isLeap(year) {
|
||||
return 29
|
||||
}
|
||||
return int(daysBefore[m] - daysBefore[m-1])
|
||||
}
|
||||
|
||||
func isLeap(year int) bool {
|
||||
return year%4 == 0 && (year%100 != 0 || year%400 == 0)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Package toml is a library to read and write TOML documents.
|
||||
package toml
|
||||
@@ -0,0 +1,269 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pelletier/go-toml/v2/internal/danger"
|
||||
)
|
||||
|
||||
// DecodeError represents an error encountered during the parsing or decoding
|
||||
// of a TOML document.
|
||||
//
|
||||
// In addition to the error message, it contains the position in the document
|
||||
// where it happened, as well as a human-readable representation that shows
|
||||
// where the error occurred in the document.
|
||||
type DecodeError struct {
|
||||
message string
|
||||
line int
|
||||
column int
|
||||
key Key
|
||||
|
||||
human string
|
||||
}
|
||||
|
||||
// StrictMissingError occurs in a TOML document that does not have a
|
||||
// corresponding field in the target value. It contains all the missing fields
|
||||
// in Errors.
|
||||
//
|
||||
// Emitted by Decoder when SetStrict(true) was called.
|
||||
type StrictMissingError struct {
|
||||
// One error per field that could not be found.
|
||||
Errors []DecodeError
|
||||
}
|
||||
|
||||
// Error returns the canonical string for this error.
|
||||
func (s *StrictMissingError) Error() string {
|
||||
return "strict mode: fields in the document are missing in the target struct"
|
||||
}
|
||||
|
||||
// String returns a human readable description of all errors.
|
||||
func (s *StrictMissingError) String() string {
|
||||
var buf strings.Builder
|
||||
|
||||
for i, e := range s.Errors {
|
||||
if i > 0 {
|
||||
buf.WriteString("\n---\n")
|
||||
}
|
||||
|
||||
buf.WriteString(e.String())
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
type Key []string
|
||||
|
||||
// internal version of DecodeError that is used as the base to create a
|
||||
// DecodeError with full context.
|
||||
type decodeError struct {
|
||||
highlight []byte
|
||||
message string
|
||||
key Key // optional
|
||||
}
|
||||
|
||||
func (de *decodeError) Error() string {
|
||||
return de.message
|
||||
}
|
||||
|
||||
func newDecodeError(highlight []byte, format string, args ...interface{}) error {
|
||||
return &decodeError{
|
||||
highlight: highlight,
|
||||
message: fmt.Errorf(format, args...).Error(),
|
||||
}
|
||||
}
|
||||
|
||||
// Error returns the error message contained in the DecodeError.
|
||||
func (e *DecodeError) Error() string {
|
||||
return "toml: " + e.message
|
||||
}
|
||||
|
||||
// String returns the human-readable contextualized error. This string is multi-line.
|
||||
func (e *DecodeError) String() string {
|
||||
return e.human
|
||||
}
|
||||
|
||||
// Position returns the (line, column) pair indicating where the error
|
||||
// occurred in the document. Positions are 1-indexed.
|
||||
func (e *DecodeError) Position() (row int, column int) {
|
||||
return e.line, e.column
|
||||
}
|
||||
|
||||
// Key that was being processed when the error occurred. The key is present only
|
||||
// if this DecodeError is part of a StrictMissingError.
|
||||
func (e *DecodeError) Key() Key {
|
||||
return e.key
|
||||
}
|
||||
|
||||
// decodeErrorFromHighlight creates a DecodeError referencing a highlighted
|
||||
// range of bytes from document.
|
||||
//
|
||||
// highlight needs to be a sub-slice of document, or this function panics.
|
||||
//
|
||||
// The function copies all bytes used in DecodeError, so that document and
|
||||
// highlight can be freely deallocated.
|
||||
//nolint:funlen
|
||||
func wrapDecodeError(document []byte, de *decodeError) *DecodeError {
|
||||
offset := danger.SubsliceOffset(document, de.highlight)
|
||||
|
||||
errMessage := de.Error()
|
||||
errLine, errColumn := positionAtEnd(document[:offset])
|
||||
before, after := linesOfContext(document, de.highlight, offset, 3)
|
||||
|
||||
var buf strings.Builder
|
||||
|
||||
maxLine := errLine + len(after) - 1
|
||||
lineColumnWidth := len(strconv.Itoa(maxLine))
|
||||
|
||||
// Write the lines of context strictly before the error.
|
||||
for i := len(before) - 1; i > 0; i-- {
|
||||
line := errLine - i
|
||||
buf.WriteString(formatLineNumber(line, lineColumnWidth))
|
||||
buf.WriteString("|")
|
||||
|
||||
if len(before[i]) > 0 {
|
||||
buf.WriteString(" ")
|
||||
buf.Write(before[i])
|
||||
}
|
||||
|
||||
buf.WriteRune('\n')
|
||||
}
|
||||
|
||||
// Write the document line that contains the error.
|
||||
|
||||
buf.WriteString(formatLineNumber(errLine, lineColumnWidth))
|
||||
buf.WriteString("| ")
|
||||
|
||||
if len(before) > 0 {
|
||||
buf.Write(before[0])
|
||||
}
|
||||
|
||||
buf.Write(de.highlight)
|
||||
|
||||
if len(after) > 0 {
|
||||
buf.Write(after[0])
|
||||
}
|
||||
|
||||
buf.WriteRune('\n')
|
||||
|
||||
// Write the line with the error message itself (so it does not have a line
|
||||
// number).
|
||||
|
||||
buf.WriteString(strings.Repeat(" ", lineColumnWidth))
|
||||
buf.WriteString("| ")
|
||||
|
||||
if len(before) > 0 {
|
||||
buf.WriteString(strings.Repeat(" ", len(before[0])))
|
||||
}
|
||||
|
||||
buf.WriteString(strings.Repeat("~", len(de.highlight)))
|
||||
|
||||
if len(errMessage) > 0 {
|
||||
buf.WriteString(" ")
|
||||
buf.WriteString(errMessage)
|
||||
}
|
||||
|
||||
// Write the lines of context strictly after the error.
|
||||
|
||||
for i := 1; i < len(after); i++ {
|
||||
buf.WriteRune('\n')
|
||||
line := errLine + i
|
||||
buf.WriteString(formatLineNumber(line, lineColumnWidth))
|
||||
buf.WriteString("|")
|
||||
|
||||
if len(after[i]) > 0 {
|
||||
buf.WriteString(" ")
|
||||
buf.Write(after[i])
|
||||
}
|
||||
}
|
||||
|
||||
return &DecodeError{
|
||||
message: errMessage,
|
||||
line: errLine,
|
||||
column: errColumn,
|
||||
key: de.key,
|
||||
human: buf.String(),
|
||||
}
|
||||
}
|
||||
|
||||
func formatLineNumber(line int, width int) string {
|
||||
format := "%" + strconv.Itoa(width) + "d"
|
||||
|
||||
return fmt.Sprintf(format, line)
|
||||
}
|
||||
|
||||
func linesOfContext(document []byte, highlight []byte, offset int, linesAround int) ([][]byte, [][]byte) {
|
||||
return beforeLines(document, offset, linesAround), afterLines(document, highlight, offset, linesAround)
|
||||
}
|
||||
|
||||
func beforeLines(document []byte, offset int, linesAround int) [][]byte {
|
||||
var beforeLines [][]byte
|
||||
|
||||
// Walk the document backward from the highlight to find previous lines
|
||||
// of context.
|
||||
rest := document[:offset]
|
||||
backward:
|
||||
for o := len(rest) - 1; o >= 0 && len(beforeLines) <= linesAround && len(rest) > 0; {
|
||||
switch {
|
||||
case rest[o] == '\n':
|
||||
// handle individual lines
|
||||
beforeLines = append(beforeLines, rest[o+1:])
|
||||
rest = rest[:o]
|
||||
o = len(rest) - 1
|
||||
case o == 0:
|
||||
// add the first line only if it's non-empty
|
||||
beforeLines = append(beforeLines, rest)
|
||||
|
||||
break backward
|
||||
default:
|
||||
o--
|
||||
}
|
||||
}
|
||||
|
||||
return beforeLines
|
||||
}
|
||||
|
||||
func afterLines(document []byte, highlight []byte, offset int, linesAround int) [][]byte {
|
||||
var afterLines [][]byte
|
||||
|
||||
// Walk the document forward from the highlight to find the following
|
||||
// lines of context.
|
||||
rest := document[offset+len(highlight):]
|
||||
forward:
|
||||
for o := 0; o < len(rest) && len(afterLines) <= linesAround; {
|
||||
switch {
|
||||
case rest[o] == '\n':
|
||||
// handle individual lines
|
||||
afterLines = append(afterLines, rest[:o])
|
||||
rest = rest[o+1:]
|
||||
o = 0
|
||||
|
||||
case o == len(rest)-1:
|
||||
// add last line only if it's non-empty
|
||||
afterLines = append(afterLines, rest)
|
||||
|
||||
break forward
|
||||
default:
|
||||
o++
|
||||
}
|
||||
}
|
||||
|
||||
return afterLines
|
||||
}
|
||||
|
||||
func positionAtEnd(b []byte) (row int, column int) {
|
||||
row = 1
|
||||
column = 1
|
||||
|
||||
for _, c := range b {
|
||||
if c == '\n' {
|
||||
row++
|
||||
column = 1
|
||||
} else {
|
||||
column++
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
//nolint:funlen
|
||||
func TestDecodeError(t *testing.T) {
|
||||
|
||||
examples := []struct {
|
||||
desc string
|
||||
doc [3]string
|
||||
msg string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "no context",
|
||||
doc: [3]string{"", "morning", ""},
|
||||
msg: "this is wrong",
|
||||
expected: `
|
||||
1| morning
|
||||
| ~~~~~~~ this is wrong`,
|
||||
},
|
||||
{
|
||||
desc: "one line",
|
||||
doc: [3]string{"good ", "morning", " everyone"},
|
||||
msg: "this is wrong",
|
||||
expected: `
|
||||
1| good morning everyone
|
||||
| ~~~~~~~ this is wrong`,
|
||||
},
|
||||
{
|
||||
desc: "exactly 3 lines",
|
||||
doc: [3]string{`line1
|
||||
line2
|
||||
line3
|
||||
before `, "highlighted", ` after
|
||||
post line 1
|
||||
post line 2
|
||||
post line 3`},
|
||||
msg: "this is wrong",
|
||||
expected: `
|
||||
1| line1
|
||||
2| line2
|
||||
3| line3
|
||||
4| before highlighted after
|
||||
| ~~~~~~~~~~~ this is wrong
|
||||
5| post line 1
|
||||
6| post line 2
|
||||
7| post line 3`,
|
||||
},
|
||||
{
|
||||
desc: "more than 3 lines",
|
||||
doc: [3]string{`should not be seen1
|
||||
should not be seen2
|
||||
line1
|
||||
line2
|
||||
line3
|
||||
before `, "highlighted", ` after
|
||||
post line 1
|
||||
post line 2
|
||||
post line 3
|
||||
should not be seen3
|
||||
should not be seen4`},
|
||||
msg: "this is wrong",
|
||||
expected: `
|
||||
3| line1
|
||||
4| line2
|
||||
5| line3
|
||||
6| before highlighted after
|
||||
| ~~~~~~~~~~~ this is wrong
|
||||
7| post line 1
|
||||
8| post line 2
|
||||
9| post line 3`,
|
||||
},
|
||||
{
|
||||
desc: "more than 10 total lines",
|
||||
doc: [3]string{`should not be seen 0
|
||||
should not be seen1
|
||||
should not be seen2
|
||||
should not be seen3
|
||||
line1
|
||||
line2
|
||||
line3
|
||||
before `, "highlighted", ` after
|
||||
post line 1
|
||||
post line 2
|
||||
post line 3
|
||||
should not be seen3
|
||||
should not be seen4`},
|
||||
msg: "this is wrong",
|
||||
expected: `
|
||||
5| line1
|
||||
6| line2
|
||||
7| line3
|
||||
8| before highlighted after
|
||||
| ~~~~~~~~~~~ this is wrong
|
||||
9| post line 1
|
||||
10| post line 2
|
||||
11| post line 3`,
|
||||
},
|
||||
{
|
||||
desc: "last line of more than 10",
|
||||
doc: [3]string{`should not be seen
|
||||
should not be seen
|
||||
should not be seen
|
||||
should not be seen
|
||||
should not be seen
|
||||
should not be seen
|
||||
should not be seen
|
||||
line1
|
||||
line2
|
||||
line3
|
||||
before `, "highlighted", ``},
|
||||
msg: "this is wrong",
|
||||
expected: `
|
||||
8| line1
|
||||
9| line2
|
||||
10| line3
|
||||
11| before highlighted
|
||||
| ~~~~~~~~~~~ this is wrong
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "handle empty lines in the before/after blocks",
|
||||
doc: [3]string{
|
||||
`line1
|
||||
|
||||
line 2
|
||||
before `, "highlighted", ` after
|
||||
line 3
|
||||
|
||||
line 4
|
||||
line 5`,
|
||||
},
|
||||
expected: `1| line1
|
||||
2|
|
||||
3| line 2
|
||||
4| before highlighted after
|
||||
| ~~~~~~~~~~~
|
||||
5| line 3
|
||||
6|
|
||||
7| line 4`,
|
||||
},
|
||||
{
|
||||
desc: "handle remainder of the error line when there is only one line",
|
||||
doc: [3]string{`P=`, `[`, `#`},
|
||||
msg: "array is incomplete",
|
||||
expected: `1| P=[#
|
||||
| ~ array is incomplete`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range examples {
|
||||
e := e
|
||||
t.Run(e.desc, func(t *testing.T) {
|
||||
|
||||
b := bytes.Buffer{}
|
||||
b.Write([]byte(e.doc[0]))
|
||||
start := b.Len()
|
||||
b.Write([]byte(e.doc[1]))
|
||||
end := b.Len()
|
||||
b.Write([]byte(e.doc[2]))
|
||||
doc := b.Bytes()
|
||||
hl := doc[start:end]
|
||||
|
||||
err := wrapDecodeError(doc, &decodeError{
|
||||
highlight: hl,
|
||||
message: e.msg,
|
||||
})
|
||||
|
||||
var derr *DecodeError
|
||||
if !errors.As(err, &derr) {
|
||||
t.Errorf("error not in expected format")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, strings.Trim(e.expected, "\n"), derr.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeError_Accessors(t *testing.T) {
|
||||
|
||||
e := DecodeError{
|
||||
message: "foo",
|
||||
line: 1,
|
||||
column: 2,
|
||||
key: []string{"one", "two"},
|
||||
human: "bar",
|
||||
}
|
||||
assert.Equal(t, "toml: foo", e.Error())
|
||||
r, c := e.Position()
|
||||
assert.Equal(t, 1, r)
|
||||
assert.Equal(t, 2, c)
|
||||
assert.Equal(t, Key{"one", "two"}, e.Key())
|
||||
assert.Equal(t, "bar", e.String())
|
||||
}
|
||||
|
||||
func ExampleDecodeError() {
|
||||
doc := `name = 123__456`
|
||||
|
||||
s := map[string]interface{}{}
|
||||
err := Unmarshal([]byte(doc), &s)
|
||||
|
||||
fmt.Println(err)
|
||||
|
||||
//nolint:errorlint
|
||||
de := err.(*DecodeError)
|
||||
fmt.Println(de.String())
|
||||
|
||||
row, col := de.Position()
|
||||
fmt.Println("error occurred at row", row, "column", col)
|
||||
// Output:
|
||||
// toml: number must have at least one digit between underscores
|
||||
// 1| name = 123__456
|
||||
// | ~~ number must have at least one digit between underscores
|
||||
// error occurred at row 1 column 11
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
# This is a TOML document. Boom.
|
||||
|
||||
title = "TOML Example"
|
||||
|
||||
[owner]
|
||||
name = "Tom Preston-Werner"
|
||||
organization = "GitHub"
|
||||
bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
|
||||
dob = 1979-05-27T07:32:00Z # First class dates? Why not?
|
||||
|
||||
[database]
|
||||
server = "192.168.1.1"
|
||||
ports = [ 8001, 8001, 8002 ]
|
||||
connection_max = 5000
|
||||
enabled = true
|
||||
|
||||
[servers]
|
||||
|
||||
# You can indent as you please. Tabs or spaces. TOML don't care.
|
||||
[servers.alpha]
|
||||
ip = "10.0.0.1"
|
||||
dc = "eqdc10"
|
||||
|
||||
[servers.beta]
|
||||
ip = "10.0.0.2"
|
||||
dc = "eqdc10"
|
||||
|
||||
[clients]
|
||||
data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package toml_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFastSimple(t *testing.T) {
|
||||
m := map[string]int64{}
|
||||
err := toml.Unmarshal([]byte(`a = 42`), &m)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]int64{"a": 42}, m)
|
||||
}
|
||||
|
||||
func TestFastSimpleString(t *testing.T) {
|
||||
m := map[string]string{}
|
||||
err := toml.Unmarshal([]byte(`a = "hello"`), &m)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]string{"a": "hello"}, m)
|
||||
}
|
||||
|
||||
func TestFastSimpleInterface(t *testing.T) {
|
||||
m := map[string]interface{}{}
|
||||
err := toml.Unmarshal([]byte(`
|
||||
a = "hello"
|
||||
b = 42`), &m)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]interface{}{
|
||||
"a": "hello",
|
||||
"b": int64(42),
|
||||
}, m)
|
||||
}
|
||||
|
||||
func TestFastMultipartKeyInterface(t *testing.T) {
|
||||
m := map[string]interface{}{}
|
||||
err := toml.Unmarshal([]byte(`
|
||||
a.interim = "test"
|
||||
a.b.c = "hello"
|
||||
b = 42`), &m)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]interface{}{
|
||||
"a": map[string]interface{}{
|
||||
"interim": "test",
|
||||
"b": map[string]interface{}{
|
||||
"c": "hello",
|
||||
},
|
||||
},
|
||||
"b": int64(42),
|
||||
}, m)
|
||||
}
|
||||
|
||||
func TestFastExistingMap(t *testing.T) {
|
||||
m := map[string]interface{}{
|
||||
"ints": map[string]int{},
|
||||
}
|
||||
err := toml.Unmarshal([]byte(`
|
||||
ints.one = 1
|
||||
ints.two = 2
|
||||
strings.yo = "hello"`), &m)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]interface{}{
|
||||
"ints": map[string]interface{}{
|
||||
"one": int64(1),
|
||||
"two": int64(2),
|
||||
},
|
||||
"strings": map[string]interface{}{
|
||||
"yo": "hello",
|
||||
},
|
||||
}, m)
|
||||
}
|
||||
|
||||
func TestFastArrayTable(t *testing.T) {
|
||||
b := []byte(`
|
||||
[root]
|
||||
[[root.nested]]
|
||||
name = 'Bob'
|
||||
[[root.nested]]
|
||||
name = 'Alice'
|
||||
`)
|
||||
|
||||
m := map[string]interface{}{}
|
||||
|
||||
err := toml.Unmarshal(b, &m)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, map[string]interface{}{
|
||||
"root": map[string]interface{}{
|
||||
"nested": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "Bob",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"name": "Alice",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, m)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module github.com/pelletier/go-toml/v2
|
||||
|
||||
go 1.16
|
||||
|
||||
// latest (v1.7.0) doesn't have the fix for time.Time
|
||||
require github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942
|
||||
@@ -0,0 +1,11 @@
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942 h1:t0lM6y/M5IiUZyvbBTcngso8SZEZICH7is9B6g/obVU=
|
||||
github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,145 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"unsafe"
|
||||
|
||||
"github.com/pelletier/go-toml/v2/internal/danger"
|
||||
)
|
||||
|
||||
// Iterator starts uninitialized, you need to call Next() first.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// it := n.Children()
|
||||
// for it.Next() {
|
||||
// it.Node()
|
||||
// }
|
||||
type Iterator struct {
|
||||
started bool
|
||||
node *Node
|
||||
}
|
||||
|
||||
// Next moves the iterator forward and returns true if points to a node, false
|
||||
// otherwise.
|
||||
func (c *Iterator) Next() bool {
|
||||
if !c.started {
|
||||
c.started = true
|
||||
} else if c.node.Valid() {
|
||||
c.node = c.node.Next()
|
||||
}
|
||||
return c.node.Valid()
|
||||
}
|
||||
|
||||
// IsLast returns true if the current node of the iterator is the last one.
|
||||
// Subsequent call to Next() will return false.
|
||||
func (c *Iterator) IsLast() bool {
|
||||
return c.node.next == 0
|
||||
}
|
||||
|
||||
// Node returns a copy of the node pointed at by the iterator.
|
||||
func (c *Iterator) Node() *Node {
|
||||
return c.node
|
||||
}
|
||||
|
||||
// Root contains a full AST.
|
||||
//
|
||||
// It is immutable once constructed with Builder.
|
||||
type Root struct {
|
||||
nodes []Node
|
||||
}
|
||||
|
||||
// Iterator over the top level nodes.
|
||||
func (r *Root) Iterator() Iterator {
|
||||
it := Iterator{}
|
||||
if len(r.nodes) > 0 {
|
||||
it.node = &r.nodes[0]
|
||||
}
|
||||
return it
|
||||
}
|
||||
|
||||
func (r *Root) at(idx Reference) *Node {
|
||||
return &r.nodes[idx]
|
||||
}
|
||||
|
||||
// Arrays have one child per element in the array.
|
||||
// InlineTables have one child per key-value pair in the table.
|
||||
// KeyValues have at least two children. The first one is the value. The
|
||||
// rest make a potentially dotted key.
|
||||
// Table and Array table have one child per element of the key they
|
||||
// represent (same as KeyValue, but without the last node being the value).
|
||||
// children []Node
|
||||
type Node struct {
|
||||
Kind Kind
|
||||
Raw Range // Raw bytes from the input.
|
||||
Data []byte // Node value (could be either allocated or referencing the input).
|
||||
|
||||
// References to other nodes, as offsets in the backing array from this
|
||||
// node. References can go backward, so those can be negative.
|
||||
next int // 0 if last element
|
||||
child int // 0 if no child
|
||||
}
|
||||
|
||||
type Range struct {
|
||||
Offset uint32
|
||||
Length uint32
|
||||
}
|
||||
|
||||
// Next returns a copy of the next node, or an invalid Node if there is no
|
||||
// next node.
|
||||
func (n *Node) Next() *Node {
|
||||
if n.next == 0 {
|
||||
return nil
|
||||
}
|
||||
ptr := unsafe.Pointer(n)
|
||||
size := unsafe.Sizeof(Node{})
|
||||
return (*Node)(danger.Stride(ptr, size, n.next))
|
||||
}
|
||||
|
||||
// Child returns a copy of the first child node of this node. Other children
|
||||
// can be accessed calling Next on the first child.
|
||||
// Returns an invalid Node if there is none.
|
||||
func (n *Node) Child() *Node {
|
||||
if n.child == 0 {
|
||||
return nil
|
||||
}
|
||||
ptr := unsafe.Pointer(n)
|
||||
size := unsafe.Sizeof(Node{})
|
||||
return (*Node)(danger.Stride(ptr, size, n.child))
|
||||
}
|
||||
|
||||
// Valid returns true if the node's kind is set (not to Invalid).
|
||||
func (n *Node) Valid() bool {
|
||||
return n != nil
|
||||
}
|
||||
|
||||
// Key returns the child nodes making the Key on a supported node. Panics
|
||||
// otherwise.
|
||||
// They are guaranteed to be all be of the Kind Key. A simple key would return
|
||||
// just one element.
|
||||
func (n *Node) Key() Iterator {
|
||||
switch n.Kind {
|
||||
case KeyValue:
|
||||
value := n.Child()
|
||||
if !value.Valid() {
|
||||
panic(fmt.Errorf("KeyValue should have at least two children"))
|
||||
}
|
||||
return Iterator{node: value.Next()}
|
||||
case Table, ArrayTable:
|
||||
return Iterator{node: n.Child()}
|
||||
default:
|
||||
panic(fmt.Errorf("Key() is not supported on a %s", n.Kind))
|
||||
}
|
||||
}
|
||||
|
||||
// Value returns a pointer to the value node of a KeyValue.
|
||||
// Guaranteed to be non-nil.
|
||||
// Panics if not called on a KeyValue node, or if the Children are malformed.
|
||||
func (n *Node) Value() *Node {
|
||||
return n.Child()
|
||||
}
|
||||
|
||||
// Children returns an iterator over a node's children.
|
||||
func (n *Node) Children() Iterator {
|
||||
return Iterator{node: n.Child()}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package ast
|
||||
|
||||
type Reference int
|
||||
|
||||
const InvalidReference Reference = -1
|
||||
|
||||
func (r Reference) Valid() bool {
|
||||
return r != InvalidReference
|
||||
}
|
||||
|
||||
type Builder struct {
|
||||
tree Root
|
||||
lastIdx int
|
||||
}
|
||||
|
||||
func (b *Builder) Tree() *Root {
|
||||
return &b.tree
|
||||
}
|
||||
|
||||
func (b *Builder) NodeAt(ref Reference) *Node {
|
||||
return b.tree.at(ref)
|
||||
}
|
||||
|
||||
func (b *Builder) Reset() {
|
||||
b.tree.nodes = b.tree.nodes[:0]
|
||||
b.lastIdx = 0
|
||||
}
|
||||
|
||||
func (b *Builder) Push(n Node) Reference {
|
||||
b.lastIdx = len(b.tree.nodes)
|
||||
b.tree.nodes = append(b.tree.nodes, n)
|
||||
return Reference(b.lastIdx)
|
||||
}
|
||||
|
||||
func (b *Builder) PushAndChain(n Node) Reference {
|
||||
newIdx := len(b.tree.nodes)
|
||||
b.tree.nodes = append(b.tree.nodes, n)
|
||||
if b.lastIdx >= 0 {
|
||||
b.tree.nodes[b.lastIdx].next = newIdx - b.lastIdx
|
||||
}
|
||||
b.lastIdx = newIdx
|
||||
return Reference(b.lastIdx)
|
||||
}
|
||||
|
||||
func (b *Builder) AttachChild(parent Reference, child Reference) {
|
||||
b.tree.nodes[parent].child = int(child) - int(parent)
|
||||
}
|
||||
|
||||
func (b *Builder) Chain(from Reference, to Reference) {
|
||||
b.tree.nodes[from].next = int(to) - int(from)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package ast
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Kind int
|
||||
|
||||
const (
|
||||
// meta
|
||||
Invalid Kind = iota
|
||||
Comment
|
||||
Key
|
||||
|
||||
// top level structures
|
||||
Table
|
||||
ArrayTable
|
||||
KeyValue
|
||||
|
||||
// containers values
|
||||
Array
|
||||
InlineTable
|
||||
|
||||
// values
|
||||
String
|
||||
Bool
|
||||
Float
|
||||
Integer
|
||||
LocalDate
|
||||
LocalTime
|
||||
LocalDateTime
|
||||
DateTime
|
||||
)
|
||||
|
||||
func (k Kind) String() string {
|
||||
switch k {
|
||||
case Invalid:
|
||||
return "Invalid"
|
||||
case Comment:
|
||||
return "Comment"
|
||||
case Key:
|
||||
return "Key"
|
||||
case Table:
|
||||
return "Table"
|
||||
case ArrayTable:
|
||||
return "ArrayTable"
|
||||
case KeyValue:
|
||||
return "KeyValue"
|
||||
case Array:
|
||||
return "Array"
|
||||
case InlineTable:
|
||||
return "InlineTable"
|
||||
case String:
|
||||
return "String"
|
||||
case Bool:
|
||||
return "Bool"
|
||||
case Float:
|
||||
return "Float"
|
||||
case Integer:
|
||||
return "Integer"
|
||||
case LocalDate:
|
||||
return "LocalDate"
|
||||
case LocalTime:
|
||||
return "LocalTime"
|
||||
case LocalDateTime:
|
||||
return "LocalDateTime"
|
||||
case DateTime:
|
||||
return "DateTime"
|
||||
}
|
||||
panic(fmt.Errorf("Kind.String() not implemented for '%d'", k))
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package danger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const maxInt = uintptr(int(^uint(0) >> 1))
|
||||
|
||||
func SubsliceOffset(data []byte, subslice []byte) int {
|
||||
datap := (*reflect.SliceHeader)(unsafe.Pointer(&data))
|
||||
hlp := (*reflect.SliceHeader)(unsafe.Pointer(&subslice))
|
||||
|
||||
if hlp.Data < datap.Data {
|
||||
panic(fmt.Errorf("subslice address (%d) is before data address (%d)", hlp.Data, datap.Data))
|
||||
}
|
||||
offset := hlp.Data - datap.Data
|
||||
|
||||
if offset > maxInt {
|
||||
panic(fmt.Errorf("slice offset larger than int (%d)", offset))
|
||||
}
|
||||
|
||||
intoffset := int(offset)
|
||||
|
||||
if intoffset > datap.Len {
|
||||
panic(fmt.Errorf("slice offset (%d) is farther than data length (%d)", intoffset, datap.Len))
|
||||
}
|
||||
|
||||
if intoffset+hlp.Len > datap.Len {
|
||||
panic(fmt.Errorf("slice ends (%d+%d) is farther than data length (%d)", intoffset, hlp.Len, datap.Len))
|
||||
}
|
||||
|
||||
return intoffset
|
||||
}
|
||||
|
||||
func BytesRange(start []byte, end []byte) []byte {
|
||||
if start == nil || end == nil {
|
||||
panic("cannot call BytesRange with nil")
|
||||
}
|
||||
startp := (*reflect.SliceHeader)(unsafe.Pointer(&start))
|
||||
endp := (*reflect.SliceHeader)(unsafe.Pointer(&end))
|
||||
|
||||
if startp.Data > endp.Data {
|
||||
panic(fmt.Errorf("start pointer address (%d) is after end pointer address (%d)", startp.Data, endp.Data))
|
||||
}
|
||||
|
||||
l := startp.Len
|
||||
endLen := int(endp.Data-startp.Data) + endp.Len
|
||||
if endLen > l {
|
||||
l = endLen
|
||||
}
|
||||
|
||||
if l > startp.Cap {
|
||||
panic(fmt.Errorf("range length is larger than capacity"))
|
||||
}
|
||||
|
||||
return start[:l]
|
||||
}
|
||||
|
||||
func Stride(ptr unsafe.Pointer, size uintptr, offset int) unsafe.Pointer {
|
||||
// TODO: replace with unsafe.Add when Go 1.17 is released
|
||||
// https://github.com/golang/go/issues/40481
|
||||
return unsafe.Pointer(uintptr(ptr) + uintptr(int(size)*offset))
|
||||
}
|
||||
|
||||
type Slice struct {
|
||||
Data unsafe.Pointer
|
||||
Len int
|
||||
Cap int
|
||||
}
|
||||
|
||||
type iface struct {
|
||||
typ unsafe.Pointer
|
||||
ptr unsafe.Pointer
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package danger_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/pelletier/go-toml/v2/internal/danger"
|
||||
)
|
||||
|
||||
func TestSubsliceOffsetValid(t *testing.T) {
|
||||
examples := []struct {
|
||||
desc string
|
||||
test func() ([]byte, []byte)
|
||||
offset int
|
||||
}{
|
||||
{
|
||||
desc: "simple",
|
||||
test: func() ([]byte, []byte) {
|
||||
data := []byte("hello")
|
||||
return data, data[1:]
|
||||
},
|
||||
offset: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range examples {
|
||||
t.Run(e.desc, func(t *testing.T) {
|
||||
d, s := e.test()
|
||||
offset := danger.SubsliceOffset(d, s)
|
||||
assert.Equal(t, e.offset, offset)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubsliceOffsetInvalid(t *testing.T) {
|
||||
examples := []struct {
|
||||
desc string
|
||||
test func() ([]byte, []byte)
|
||||
}{
|
||||
{
|
||||
desc: "unrelated arrays",
|
||||
test: func() ([]byte, []byte) {
|
||||
return []byte("one"), []byte("two")
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "slice starts before data",
|
||||
test: func() ([]byte, []byte) {
|
||||
full := []byte("hello world")
|
||||
return full[5:], full[1:]
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "slice starts after data",
|
||||
test: func() ([]byte, []byte) {
|
||||
full := []byte("hello world")
|
||||
return full[:3], full[5:]
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "slice ends after data",
|
||||
test: func() ([]byte, []byte) {
|
||||
full := []byte("hello world")
|
||||
return full[:5], full[3:8]
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range examples {
|
||||
t.Run(e.desc, func(t *testing.T) {
|
||||
d, s := e.test()
|
||||
require.Panics(t, func() {
|
||||
danger.SubsliceOffset(d, s)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStride(t *testing.T) {
|
||||
a := []byte{1, 2, 3, 4}
|
||||
x := &a[1]
|
||||
n := (*byte)(danger.Stride(unsafe.Pointer(x), unsafe.Sizeof(byte(0)), 1))
|
||||
require.Equal(t, &a[2], n)
|
||||
n = (*byte)(danger.Stride(unsafe.Pointer(x), unsafe.Sizeof(byte(0)), -1))
|
||||
require.Equal(t, &a[0], n)
|
||||
}
|
||||
|
||||
func TestBytesRange(t *testing.T) {
|
||||
type fn = func() ([]byte, []byte)
|
||||
examples := []struct {
|
||||
desc string
|
||||
test fn
|
||||
expected []byte
|
||||
}{
|
||||
{
|
||||
desc: "simple",
|
||||
test: func() ([]byte, []byte) {
|
||||
full := []byte("hello world")
|
||||
return full[1:3], full[6:8]
|
||||
},
|
||||
expected: []byte("ello wo"),
|
||||
},
|
||||
{
|
||||
desc: "full",
|
||||
test: func() ([]byte, []byte) {
|
||||
full := []byte("hello world")
|
||||
return full[0:1], full[len(full)-1:]
|
||||
},
|
||||
expected: []byte("hello world"),
|
||||
},
|
||||
{
|
||||
desc: "end before start",
|
||||
test: func() ([]byte, []byte) {
|
||||
full := []byte("hello world")
|
||||
return full[len(full)-1:], full[0:1]
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "nils",
|
||||
test: func() ([]byte, []byte) {
|
||||
return nil, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "nils start",
|
||||
test: func() ([]byte, []byte) {
|
||||
return nil, []byte("foo")
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "nils end",
|
||||
test: func() ([]byte, []byte) {
|
||||
return []byte("foo"), nil
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "start is end",
|
||||
test: func() ([]byte, []byte) {
|
||||
full := []byte("hello world")
|
||||
return full[1:3], full[1:3]
|
||||
},
|
||||
expected: []byte("el"),
|
||||
},
|
||||
{
|
||||
desc: "end contained in start",
|
||||
test: func() ([]byte, []byte) {
|
||||
full := []byte("hello world")
|
||||
return full[1:7], full[2:4]
|
||||
},
|
||||
expected: []byte("ello w"),
|
||||
},
|
||||
{
|
||||
desc: "different backing arrays",
|
||||
test: func() ([]byte, []byte) {
|
||||
one := []byte("hello world")
|
||||
two := []byte("hello world")
|
||||
return one, two
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range examples {
|
||||
t.Run(e.desc, func(t *testing.T) {
|
||||
start, end := e.test()
|
||||
if e.expected == nil {
|
||||
require.Panics(t, func() {
|
||||
danger.BytesRange(start, end)
|
||||
})
|
||||
} else {
|
||||
res := danger.BytesRange(start, end)
|
||||
require.Equal(t, e.expected, res)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//go:build go1.18
|
||||
// +build go1.18
|
||||
|
||||
package danger
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func ExtendSlice(t reflect.Type, s *Slice, n int) Slice {
|
||||
arrayType := reflect.ArrayOf(n, t.Elem())
|
||||
arrayData := reflect.New(arrayType)
|
||||
reflect.Copy(arrayData.Elem(), reflect.NewAt(t, unsafe.Pointer(s)).Elem())
|
||||
return Slice{
|
||||
Data: unsafe.Pointer(arrayData.Pointer()),
|
||||
Len: s.Len,
|
||||
Cap: n,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//go:build !go1.18
|
||||
// +build !go1.18
|
||||
|
||||
package danger
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
//go:linkname unsafe_NewArray reflect.unsafe_NewArray
|
||||
func unsafe_NewArray(rtype unsafe.Pointer, length int) unsafe.Pointer
|
||||
|
||||
//go:linkname typedslicecopy reflect.typedslicecopy
|
||||
//go:noescape
|
||||
func typedslicecopy(elemType unsafe.Pointer, dst, src Slice) int
|
||||
|
||||
func ExtendSlice(t reflect.Type, s *Slice, n int) Slice {
|
||||
elemTypeRef := t.Elem()
|
||||
elemTypePtr := ((*iface)(unsafe.Pointer(&elemTypeRef))).ptr
|
||||
|
||||
d := Slice{
|
||||
Data: unsafe_NewArray(elemTypePtr, n),
|
||||
Len: s.Len,
|
||||
Cap: n,
|
||||
}
|
||||
|
||||
typedslicecopy(elemTypePtr, d, *s)
|
||||
return d
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package danger
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// typeID is used as key in encoder and decoder caches to enable using
|
||||
// the optimize runtime.mapaccess2_fast64 function instead of the more
|
||||
// expensive lookup if we were to use reflect.Type as map key.
|
||||
//
|
||||
// typeID holds the pointer to the reflect.Type value, which is unique
|
||||
// in the program.
|
||||
//
|
||||
// https://github.com/segmentio/encoding/blob/master/json/codec.go#L59-L61
|
||||
type TypeID unsafe.Pointer
|
||||
|
||||
func MakeTypeID(t reflect.Type) TypeID {
|
||||
// reflect.Type has the fields:
|
||||
// typ unsafe.Pointer
|
||||
// ptr unsafe.Pointer
|
||||
return TypeID((*[2]unsafe.Pointer)(unsafe.Pointer(&t))[1])
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package imported_tests
|
||||
|
||||
// Those tests have been imported from v1, but adjust to match the new
|
||||
// defaults of v2.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDocMarshal(t *testing.T) {
|
||||
type testDoc struct {
|
||||
Title string `toml:"title"`
|
||||
BasicLists testDocBasicLists `toml:"basic_lists"`
|
||||
SubDocPtrs []*testSubDoc `toml:"subdocptrs"`
|
||||
BasicMap map[string]string `toml:"basic_map"`
|
||||
Subdocs testDocSubs `toml:"subdoc"`
|
||||
Basics testDocBasics `toml:"basic"`
|
||||
SubDocList []testSubDoc `toml:"subdoclist"`
|
||||
err int `toml:"shouldntBeHere"`
|
||||
unexported int `toml:"shouldntBeHere"`
|
||||
Unexported2 int `toml:"-"`
|
||||
}
|
||||
|
||||
var docData = testDoc{
|
||||
Title: "TOML Marshal Testing",
|
||||
unexported: 0,
|
||||
Unexported2: 0,
|
||||
Basics: testDocBasics{
|
||||
Bool: true,
|
||||
Date: time.Date(1979, 5, 27, 7, 32, 0, 0, time.UTC),
|
||||
Float32: 123.4,
|
||||
Float64: 123.456782132399,
|
||||
Int: 5000,
|
||||
Uint: 5001,
|
||||
String: &biteMe,
|
||||
unexported: 0,
|
||||
},
|
||||
BasicLists: testDocBasicLists{
|
||||
Floats: []*float32{&float1, &float2, &float3},
|
||||
Bools: []bool{true, false, true},
|
||||
Dates: []time.Time{
|
||||
time.Date(1979, 5, 27, 7, 32, 0, 0, time.UTC),
|
||||
time.Date(1980, 5, 27, 7, 32, 0, 0, time.UTC),
|
||||
},
|
||||
Ints: []int{8001, 8001, 8002},
|
||||
Strings: []string{"One", "Two", "Three"},
|
||||
UInts: []uint{5002, 5003},
|
||||
},
|
||||
BasicMap: map[string]string{
|
||||
"one": "one",
|
||||
"two": "two",
|
||||
},
|
||||
Subdocs: testDocSubs{
|
||||
First: testSubDoc{"First", 0},
|
||||
Second: &subdoc,
|
||||
},
|
||||
SubDocList: []testSubDoc{
|
||||
{"List.First", 0},
|
||||
{"List.Second", 0},
|
||||
},
|
||||
SubDocPtrs: []*testSubDoc{&subdoc},
|
||||
}
|
||||
|
||||
marshalTestToml := `title = 'TOML Marshal Testing'
|
||||
[basic_lists]
|
||||
floats = [12.3, 45.6, 78.9]
|
||||
bools = [true, false, true]
|
||||
dates = [1979-05-27T07:32:00Z, 1980-05-27T07:32:00Z]
|
||||
ints = [8001, 8001, 8002]
|
||||
uints = [5002, 5003]
|
||||
strings = ['One', 'Two', 'Three']
|
||||
|
||||
[[subdocptrs]]
|
||||
name = 'Second'
|
||||
|
||||
[basic_map]
|
||||
one = 'one'
|
||||
two = 'two'
|
||||
|
||||
[subdoc]
|
||||
[subdoc.second]
|
||||
name = 'Second'
|
||||
|
||||
[subdoc.first]
|
||||
name = 'First'
|
||||
|
||||
|
||||
[basic]
|
||||
uint = 5001
|
||||
bool = true
|
||||
float = 123.4
|
||||
float64 = 123.456782132399
|
||||
int = 5000
|
||||
string = 'Bite me'
|
||||
date = 1979-05-27T07:32:00Z
|
||||
|
||||
[[subdoclist]]
|
||||
name = 'List.First'
|
||||
[[subdoclist]]
|
||||
name = 'List.Second'
|
||||
|
||||
`
|
||||
|
||||
result, err := toml.Marshal(docData)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, marshalTestToml, string(result))
|
||||
}
|
||||
|
||||
func TestBasicMarshalQuotedKey(t *testing.T) {
|
||||
result, err := toml.Marshal(quotedKeyMarshalTestData)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := `'Z.string-àéù' = 'Hello'
|
||||
'Yfloat-𝟘' = 3.5
|
||||
['Xsubdoc-àéù']
|
||||
String2 = 'One'
|
||||
|
||||
[['W.sublist-𝟘']]
|
||||
String2 = 'Two'
|
||||
[['W.sublist-𝟘']]
|
||||
String2 = 'Three'
|
||||
|
||||
`
|
||||
|
||||
require.Equal(t, string(expected), string(result))
|
||||
|
||||
}
|
||||
|
||||
func TestEmptyMarshal(t *testing.T) {
|
||||
type emptyMarshalTestStruct struct {
|
||||
Title string `toml:"title"`
|
||||
Bool bool `toml:"bool"`
|
||||
Int int `toml:"int"`
|
||||
String string `toml:"string"`
|
||||
StringList []string `toml:"stringlist"`
|
||||
Ptr *basicMarshalTestStruct `toml:"ptr"`
|
||||
Map map[string]string `toml:"map"`
|
||||
}
|
||||
|
||||
doc := emptyMarshalTestStruct{
|
||||
Title: "Placeholder",
|
||||
Bool: false,
|
||||
Int: 0,
|
||||
String: "",
|
||||
StringList: []string{},
|
||||
Ptr: nil,
|
||||
Map: map[string]string{},
|
||||
}
|
||||
result, err := toml.Marshal(doc)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := `title = 'Placeholder'
|
||||
bool = false
|
||||
int = 0
|
||||
string = ''
|
||||
stringlist = []
|
||||
[map]
|
||||
|
||||
`
|
||||
|
||||
require.Equal(t, string(expected), string(result))
|
||||
}
|
||||
|
||||
type textMarshaler struct {
|
||||
FirstName string
|
||||
LastName string
|
||||
}
|
||||
|
||||
func (m textMarshaler) MarshalText() ([]byte, error) {
|
||||
fullName := fmt.Sprintf("%s %s", m.FirstName, m.LastName)
|
||||
return []byte(fullName), nil
|
||||
}
|
||||
|
||||
func TestTextMarshaler(t *testing.T) {
|
||||
type wrap struct {
|
||||
TM textMarshaler
|
||||
}
|
||||
|
||||
m := textMarshaler{FirstName: "Sally", LastName: "Fields"}
|
||||
|
||||
t.Run("at root", func(t *testing.T) {
|
||||
_, err := toml.Marshal(m)
|
||||
// in v2 we do not allow TextMarshaler at root
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("leaf", func(t *testing.T) {
|
||||
res, err := toml.Marshal(wrap{m})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "TM = 'Sally Fields'\n", string(res))
|
||||
})
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"github.com/pelletier/go-toml/v2/internal/ast"
|
||||
)
|
||||
|
||||
// KeyTracker is a tracker that keeps track of the current Key as the AST is
|
||||
// walked.
|
||||
type KeyTracker struct {
|
||||
k []string
|
||||
}
|
||||
|
||||
// UpdateTable sets the state of the tracker with the AST table node.
|
||||
func (t *KeyTracker) UpdateTable(node *ast.Node) {
|
||||
t.reset()
|
||||
t.Push(node)
|
||||
}
|
||||
|
||||
// UpdateArrayTable sets the state of the tracker with the AST array table node.
|
||||
func (t *KeyTracker) UpdateArrayTable(node *ast.Node) {
|
||||
t.reset()
|
||||
t.Push(node)
|
||||
}
|
||||
|
||||
// Push the given key on the stack.
|
||||
func (t *KeyTracker) Push(node *ast.Node) {
|
||||
it := node.Key()
|
||||
for it.Next() {
|
||||
t.k = append(t.k, string(it.Node().Data))
|
||||
}
|
||||
}
|
||||
|
||||
// Pop key from stack.
|
||||
func (t *KeyTracker) Pop(node *ast.Node) {
|
||||
it := node.Key()
|
||||
for it.Next() {
|
||||
t.k = t.k[:len(t.k)-1]
|
||||
}
|
||||
}
|
||||
|
||||
// Key returns the current key
|
||||
func (t *KeyTracker) Key() []string {
|
||||
k := make([]string, len(t.k))
|
||||
copy(k, t.k)
|
||||
return k
|
||||
}
|
||||
|
||||
func (t *KeyTracker) reset() {
|
||||
t.k = t.k[:0]
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/pelletier/go-toml/v2/internal/ast"
|
||||
)
|
||||
|
||||
type keyKind uint8
|
||||
|
||||
const (
|
||||
invalidKind keyKind = iota
|
||||
valueKind
|
||||
tableKind
|
||||
arrayTableKind
|
||||
)
|
||||
|
||||
func (k keyKind) String() string {
|
||||
switch k {
|
||||
case invalidKind:
|
||||
return "invalid"
|
||||
case valueKind:
|
||||
return "value"
|
||||
case tableKind:
|
||||
return "table"
|
||||
case arrayTableKind:
|
||||
return "array table"
|
||||
}
|
||||
panic("missing keyKind string mapping")
|
||||
}
|
||||
|
||||
// SeenTracker tracks which keys have been seen with which TOML type to flag
|
||||
// duplicates and mismatches according to the spec.
|
||||
//
|
||||
// Each node in the visited tree is represented by an entry. Each entry has an
|
||||
// identifier, which is provided by a counter. Entries are stored in the array
|
||||
// entries. As new nodes are discovered (referenced for the first time in the
|
||||
// TOML document), entries are created and appended to the array. An entry
|
||||
// points to its parent using its id.
|
||||
//
|
||||
// To find whether a given key (sequence of []byte) has already been visited,
|
||||
// the entries are linearly searched, looking for one with the right name and
|
||||
// parent id.
|
||||
//
|
||||
// Given that all keys appear in the document after their parent, it is
|
||||
// guaranteed that all descendants of a node are stored after the node, this
|
||||
// speeds up the search process.
|
||||
//
|
||||
// When encountering [[array tables]], the descendants of that node are removed
|
||||
// to allow that branch of the tree to be "rediscovered". To maintain the
|
||||
// invariant above, the deletion process needs to keep the order of entries.
|
||||
// This results in more copies in that case.
|
||||
type SeenTracker struct {
|
||||
entries []entry
|
||||
currentIdx int
|
||||
nextID int
|
||||
}
|
||||
|
||||
type entry struct {
|
||||
id int
|
||||
parent int
|
||||
name []byte
|
||||
kind keyKind
|
||||
explicit bool
|
||||
}
|
||||
|
||||
// Remove all descendants of node at position idx.
|
||||
func (s *SeenTracker) clear(idx int) {
|
||||
p := s.entries[idx].id
|
||||
rest := clear(p, s.entries[idx+1:])
|
||||
s.entries = s.entries[:idx+1+len(rest)]
|
||||
}
|
||||
|
||||
func clear(parentID int, entries []entry) []entry {
|
||||
for i := 0; i < len(entries); {
|
||||
if entries[i].parent == parentID {
|
||||
id := entries[i].id
|
||||
copy(entries[i:], entries[i+1:])
|
||||
entries = entries[:len(entries)-1]
|
||||
rest := clear(id, entries[i:])
|
||||
entries = entries[:i+len(rest)]
|
||||
} else {
|
||||
i++
|
||||
}
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
func (s *SeenTracker) create(parentIdx int, name []byte, kind keyKind, explicit bool) int {
|
||||
parentID := s.id(parentIdx)
|
||||
|
||||
idx := len(s.entries)
|
||||
s.entries = append(s.entries, entry{
|
||||
id: s.nextID,
|
||||
parent: parentID,
|
||||
name: name,
|
||||
kind: kind,
|
||||
explicit: explicit,
|
||||
})
|
||||
s.nextID++
|
||||
return idx
|
||||
}
|
||||
|
||||
// CheckExpression takes a top-level node and checks that it does not contain
|
||||
// keys that have been seen in previous calls, and validates that types are
|
||||
// consistent.
|
||||
func (s *SeenTracker) CheckExpression(node *ast.Node) error {
|
||||
if s.entries == nil {
|
||||
// Skip ID = 0 to remove the confusion between nodes whose
|
||||
// parent has id 0 and root nodes (parent id is 0 because it's
|
||||
// the zero value).
|
||||
s.nextID = 1
|
||||
// Start unscoped, so idx is negative.
|
||||
s.currentIdx = -1
|
||||
}
|
||||
switch node.Kind {
|
||||
case ast.KeyValue:
|
||||
return s.checkKeyValue(s.currentIdx, node)
|
||||
case ast.Table:
|
||||
return s.checkTable(node)
|
||||
case ast.ArrayTable:
|
||||
return s.checkArrayTable(node)
|
||||
default:
|
||||
panic(fmt.Errorf("this should not be a top level node type: %s", node.Kind))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SeenTracker) checkTable(node *ast.Node) error {
|
||||
it := node.Key()
|
||||
|
||||
parentIdx := -1
|
||||
|
||||
// This code is duplicated in checkArrayTable. This is because factoring
|
||||
// it in a function requires to copy the iterator, or allocate it to the
|
||||
// heap, which is not cheap.
|
||||
for it.Next() {
|
||||
if it.IsLast() {
|
||||
break
|
||||
}
|
||||
|
||||
k := it.Node().Data
|
||||
|
||||
idx := s.find(parentIdx, k)
|
||||
|
||||
if idx < 0 {
|
||||
idx = s.create(parentIdx, k, tableKind, false)
|
||||
}
|
||||
parentIdx = idx
|
||||
}
|
||||
|
||||
k := it.Node().Data
|
||||
idx := s.find(parentIdx, k)
|
||||
|
||||
if idx >= 0 {
|
||||
kind := s.entries[idx].kind
|
||||
if kind != tableKind {
|
||||
return fmt.Errorf("toml: key %s should be a table, not a %s", string(k), kind)
|
||||
}
|
||||
if s.entries[idx].explicit {
|
||||
return fmt.Errorf("toml: table %s already exists", string(k))
|
||||
}
|
||||
s.entries[idx].explicit = true
|
||||
} else {
|
||||
idx = s.create(parentIdx, k, tableKind, true)
|
||||
}
|
||||
|
||||
s.currentIdx = idx
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SeenTracker) checkArrayTable(node *ast.Node) error {
|
||||
it := node.Key()
|
||||
|
||||
parentIdx := -1
|
||||
|
||||
for it.Next() {
|
||||
if it.IsLast() {
|
||||
break
|
||||
}
|
||||
|
||||
k := it.Node().Data
|
||||
|
||||
idx := s.find(parentIdx, k)
|
||||
|
||||
if idx < 0 {
|
||||
idx = s.create(parentIdx, k, tableKind, false)
|
||||
}
|
||||
parentIdx = idx
|
||||
}
|
||||
|
||||
k := it.Node().Data
|
||||
idx := s.find(parentIdx, k)
|
||||
|
||||
if idx >= 0 {
|
||||
kind := s.entries[idx].kind
|
||||
if kind != arrayTableKind {
|
||||
return fmt.Errorf("toml: key %s already exists as a %s, but should be an array table", kind, string(k))
|
||||
}
|
||||
s.clear(idx)
|
||||
} else {
|
||||
idx = s.create(parentIdx, k, arrayTableKind, true)
|
||||
}
|
||||
|
||||
s.currentIdx = idx
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SeenTracker) checkKeyValue(parentIdx int, node *ast.Node) error {
|
||||
it := node.Key()
|
||||
|
||||
for it.Next() {
|
||||
k := it.Node().Data
|
||||
|
||||
idx := s.find(parentIdx, k)
|
||||
|
||||
if idx < 0 {
|
||||
idx = s.create(parentIdx, k, tableKind, false)
|
||||
} else {
|
||||
entry := s.entries[idx]
|
||||
if it.IsLast() {
|
||||
return fmt.Errorf("toml: key %s is already defined", string(k))
|
||||
} else if entry.kind != tableKind {
|
||||
return fmt.Errorf("toml: expected %s to be a table, not a %s", string(k), entry.kind)
|
||||
} else if entry.explicit {
|
||||
return fmt.Errorf("toml: cannot redefine table %s that has already been explicitly defined", string(k))
|
||||
}
|
||||
}
|
||||
|
||||
parentIdx = idx
|
||||
}
|
||||
|
||||
s.entries[parentIdx].kind = valueKind
|
||||
|
||||
value := node.Value()
|
||||
|
||||
switch value.Kind {
|
||||
case ast.InlineTable:
|
||||
return s.checkInlineTable(parentIdx, value)
|
||||
case ast.Array:
|
||||
return s.checkArray(parentIdx, value)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SeenTracker) checkArray(parentIdx int, node *ast.Node) error {
|
||||
set := false
|
||||
it := node.Children()
|
||||
for it.Next() {
|
||||
if set {
|
||||
s.clear(parentIdx)
|
||||
}
|
||||
n := it.Node()
|
||||
switch n.Kind {
|
||||
case ast.InlineTable:
|
||||
err := s.checkInlineTable(parentIdx, n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
set = true
|
||||
case ast.Array:
|
||||
err := s.checkArray(parentIdx, n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
set = true
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SeenTracker) checkInlineTable(parentIdx int, node *ast.Node) error {
|
||||
it := node.Children()
|
||||
for it.Next() {
|
||||
n := it.Node()
|
||||
err := s.checkKeyValue(parentIdx, n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SeenTracker) id(idx int) int {
|
||||
if idx >= 0 {
|
||||
return s.entries[idx].id
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (s *SeenTracker) find(parentIdx int, k []byte) int {
|
||||
parentID := s.id(parentIdx)
|
||||
|
||||
for i := parentIdx + 1; i < len(s.entries); i++ {
|
||||
if s.entries[i].parent == parentID && bytes.Equal(s.entries[i].name, k) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package tracker
|
||||
@@ -1,436 +0,0 @@
|
||||
// TOML lexer.// Written using the principles developped by Rob Pike in
|
||||
// http://www.youtube.com/watch?v=HxaD_trXwRE
|
||||
|
||||
package toml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var dateRegexp *regexp.Regexp
|
||||
|
||||
// Define tokens
|
||||
type tokenType int
|
||||
|
||||
const (
|
||||
eof = -(iota + 1)
|
||||
)
|
||||
|
||||
const (
|
||||
tokenError tokenType = iota
|
||||
tokenEOF
|
||||
tokenComment
|
||||
tokenKey
|
||||
tokenEqual
|
||||
tokenString
|
||||
tokenInteger
|
||||
tokenTrue
|
||||
tokenFalse
|
||||
tokenFloat
|
||||
tokenLeftBracket
|
||||
tokenRightBracket
|
||||
tokenDate
|
||||
tokenKeyGroup
|
||||
tokenComma
|
||||
tokenEOL
|
||||
)
|
||||
|
||||
type token struct {
|
||||
typ tokenType
|
||||
val string
|
||||
}
|
||||
|
||||
func (i token) String() string {
|
||||
switch i.typ {
|
||||
case tokenEOF:
|
||||
return "EOF"
|
||||
case tokenError:
|
||||
return i.val
|
||||
}
|
||||
|
||||
if len(i.val) > 10 {
|
||||
return fmt.Sprintf("%.10q...", i.val)
|
||||
}
|
||||
return fmt.Sprintf("%q", i.val)
|
||||
}
|
||||
|
||||
func isSpace(r rune) bool {
|
||||
return r == ' ' || r == '\t'
|
||||
}
|
||||
|
||||
func isAlphanumeric(r rune) bool {
|
||||
return unicode.IsLetter(r) || r == '_'
|
||||
}
|
||||
|
||||
func isDigit(r rune) bool {
|
||||
return unicode.IsNumber(r)
|
||||
}
|
||||
|
||||
func isHexDigit(r rune) bool {
|
||||
return isDigit(r) ||
|
||||
r == 'A' || r == 'B' || r == 'C' || r == 'D' || r == 'E' || r == 'F'
|
||||
}
|
||||
|
||||
// Define lexer
|
||||
type lexer struct {
|
||||
input string
|
||||
start int
|
||||
pos int
|
||||
width int
|
||||
tokens chan token
|
||||
depth int
|
||||
}
|
||||
|
||||
func (l *lexer) run() {
|
||||
for state := lexVoid; state != nil; {
|
||||
state = state(l)
|
||||
}
|
||||
close(l.tokens)
|
||||
}
|
||||
|
||||
func (l *lexer) emit(t tokenType) {
|
||||
l.tokens <- token{t, l.input[l.start:l.pos]}
|
||||
l.start = l.pos
|
||||
}
|
||||
|
||||
func (l *lexer) emitWithValue(t tokenType, value string) {
|
||||
l.tokens <- token{t, value}
|
||||
l.start = l.pos
|
||||
}
|
||||
|
||||
func (l *lexer) next() rune {
|
||||
if l.pos >= len(l.input) {
|
||||
l.width = 0
|
||||
return eof
|
||||
}
|
||||
var r rune
|
||||
r, l.width = utf8.DecodeRuneInString(l.input[l.pos:])
|
||||
l.pos += l.width
|
||||
return r
|
||||
}
|
||||
|
||||
func (l *lexer) ignore() {
|
||||
l.start = l.pos
|
||||
}
|
||||
|
||||
func (l *lexer) backup() {
|
||||
l.pos -= l.width
|
||||
}
|
||||
|
||||
func (l *lexer) errorf(format string, args ...interface{}) stateFn {
|
||||
l.tokens <- token{
|
||||
tokenError,
|
||||
fmt.Sprintf(format, args...),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *lexer) peek() rune {
|
||||
r := l.next()
|
||||
l.backup()
|
||||
return r
|
||||
}
|
||||
|
||||
func (l *lexer) accept(valid string) bool {
|
||||
if strings.IndexRune(valid, l.next()) >= 0 {
|
||||
return true
|
||||
}
|
||||
l.backup()
|
||||
return false
|
||||
}
|
||||
|
||||
func (l *lexer) follow(next string) bool {
|
||||
return strings.HasPrefix(l.input[l.pos:], next)
|
||||
}
|
||||
|
||||
// Define state functions
|
||||
type stateFn func(*lexer) stateFn
|
||||
|
||||
func lexVoid(l *lexer) stateFn {
|
||||
for {
|
||||
next := l.peek()
|
||||
switch next {
|
||||
case '[':
|
||||
return lexKeyGroup
|
||||
case '#':
|
||||
return lexComment
|
||||
case '=':
|
||||
return lexEqual
|
||||
}
|
||||
|
||||
if isAlphanumeric(next) {
|
||||
return lexKey
|
||||
}
|
||||
|
||||
if isSpace(next) {
|
||||
l.ignore()
|
||||
}
|
||||
|
||||
if l.next() == eof {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
l.emit(tokenEOF)
|
||||
return nil
|
||||
}
|
||||
|
||||
func lexRvalue(l *lexer) stateFn {
|
||||
for {
|
||||
next := l.peek()
|
||||
switch next {
|
||||
case '[':
|
||||
l.depth += 1
|
||||
return lexLeftBracket
|
||||
case ']':
|
||||
l.depth -= 1
|
||||
return lexRightBracket
|
||||
case '#':
|
||||
return lexComment
|
||||
case '"':
|
||||
return lexString
|
||||
case ',':
|
||||
return lexComma
|
||||
case '\n':
|
||||
l.ignore()
|
||||
l.pos += 1
|
||||
if l.depth == 0 {
|
||||
return lexVoid
|
||||
} else {
|
||||
return lexRvalue
|
||||
}
|
||||
}
|
||||
|
||||
if l.follow("true") {
|
||||
return lexTrue
|
||||
}
|
||||
|
||||
if l.follow("false") {
|
||||
return lexFalse
|
||||
}
|
||||
|
||||
if isAlphanumeric(next) {
|
||||
return lexKey
|
||||
}
|
||||
|
||||
if dateRegexp.FindString(l.input[l.pos:]) != "" {
|
||||
return lexDate
|
||||
}
|
||||
|
||||
if next == '+' || next == '-' || isDigit(next) {
|
||||
return lexNumber
|
||||
}
|
||||
|
||||
if isSpace(next) {
|
||||
l.ignore()
|
||||
}
|
||||
|
||||
if l.next() == eof {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
l.emit(tokenEOF)
|
||||
return nil
|
||||
}
|
||||
|
||||
func lexDate(l *lexer) stateFn {
|
||||
l.ignore()
|
||||
l.pos += 20 // Fixed size of a date in TOML
|
||||
l.emit(tokenDate)
|
||||
return lexRvalue
|
||||
}
|
||||
|
||||
func lexTrue(l *lexer) stateFn {
|
||||
l.ignore()
|
||||
l.pos += 4
|
||||
l.emit(tokenTrue)
|
||||
return lexRvalue
|
||||
}
|
||||
|
||||
func lexFalse(l *lexer) stateFn {
|
||||
l.ignore()
|
||||
l.pos += 5
|
||||
l.emit(tokenFalse)
|
||||
return lexRvalue
|
||||
}
|
||||
|
||||
func lexEqual(l *lexer) stateFn {
|
||||
l.ignore()
|
||||
l.accept("=")
|
||||
l.emit(tokenEqual)
|
||||
return lexRvalue
|
||||
}
|
||||
|
||||
func lexComma(l *lexer) stateFn {
|
||||
l.ignore()
|
||||
l.accept(",")
|
||||
l.emit(tokenComma)
|
||||
return lexRvalue
|
||||
}
|
||||
|
||||
func lexKey(l *lexer) stateFn {
|
||||
l.ignore()
|
||||
for isAlphanumeric(l.next()) {
|
||||
}
|
||||
l.backup()
|
||||
l.emit(tokenKey)
|
||||
return lexVoid
|
||||
}
|
||||
|
||||
func lexComment(l *lexer) stateFn {
|
||||
for {
|
||||
next := l.next()
|
||||
if next == '\n' || next == eof {
|
||||
break
|
||||
}
|
||||
}
|
||||
l.ignore()
|
||||
return lexVoid
|
||||
}
|
||||
|
||||
func lexLeftBracket(l *lexer) stateFn {
|
||||
l.ignore()
|
||||
l.pos += 1
|
||||
l.emit(tokenLeftBracket)
|
||||
return lexRvalue
|
||||
}
|
||||
|
||||
func lexString(l *lexer) stateFn {
|
||||
l.pos += 1
|
||||
l.ignore()
|
||||
growing_string := ""
|
||||
|
||||
for {
|
||||
if l.peek() == '"' {
|
||||
l.emitWithValue(tokenString, growing_string)
|
||||
l.pos += 1
|
||||
l.ignore()
|
||||
return lexRvalue
|
||||
}
|
||||
|
||||
if l.follow("\\\"") {
|
||||
l.pos += 1
|
||||
growing_string += "\""
|
||||
} else if l.follow("\\n") {
|
||||
l.pos += 1
|
||||
growing_string += "\n"
|
||||
} else if l.follow("\\t") {
|
||||
l.pos += 1
|
||||
growing_string += "\t"
|
||||
} else if l.follow("\\r") {
|
||||
l.pos += 1
|
||||
growing_string += "\r"
|
||||
} else if l.follow("\\\\") {
|
||||
l.pos += 1
|
||||
growing_string += "\\"
|
||||
} else if l.follow("\\u") {
|
||||
l.pos += 2
|
||||
code := ""
|
||||
for i := 0; i < 4; i++ {
|
||||
c := l.peek()
|
||||
l.pos += 1
|
||||
if !isHexDigit(c) {
|
||||
return l.errorf("unfinished unicode escape")
|
||||
}
|
||||
code = code + string(c)
|
||||
}
|
||||
l.pos -= 1
|
||||
intcode, err := strconv.ParseInt(code, 16, 32)
|
||||
if err != nil {
|
||||
return l.errorf("invalid unicode escape: \\u" + code)
|
||||
}
|
||||
growing_string += string(rune(intcode))
|
||||
} else {
|
||||
growing_string += string(l.peek())
|
||||
}
|
||||
|
||||
if l.next() == eof {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return l.errorf("unclosed string")
|
||||
}
|
||||
|
||||
func lexKeyGroup(l *lexer) stateFn {
|
||||
l.ignore()
|
||||
l.pos += 1
|
||||
l.emit(tokenLeftBracket)
|
||||
return lexInsideKeyGroup
|
||||
}
|
||||
|
||||
func lexInsideKeyGroup(l *lexer) stateFn {
|
||||
for {
|
||||
if l.peek() == ']' {
|
||||
if l.pos > l.start {
|
||||
l.emit(tokenKeyGroup)
|
||||
}
|
||||
l.ignore()
|
||||
l.pos += 1
|
||||
l.emit(tokenRightBracket)
|
||||
return lexVoid
|
||||
}
|
||||
|
||||
if l.next() == eof {
|
||||
break
|
||||
}
|
||||
}
|
||||
return l.errorf("unclosed key group")
|
||||
}
|
||||
|
||||
func lexRightBracket(l *lexer) stateFn {
|
||||
l.ignore()
|
||||
l.pos += 1
|
||||
l.emit(tokenRightBracket)
|
||||
return lexRvalue
|
||||
}
|
||||
|
||||
func lexNumber(l *lexer) stateFn {
|
||||
l.ignore()
|
||||
if !l.accept("+") {
|
||||
l.accept("-")
|
||||
}
|
||||
point_seen := false
|
||||
digit_seen := false
|
||||
for {
|
||||
next := l.next()
|
||||
if next == '.' {
|
||||
point_seen = true
|
||||
} else if isDigit(next) {
|
||||
digit_seen = true
|
||||
} else {
|
||||
l.backup()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !digit_seen {
|
||||
return l.errorf("no digit in that number")
|
||||
}
|
||||
if point_seen {
|
||||
l.emit(tokenFloat)
|
||||
} else {
|
||||
l.emit(tokenInteger)
|
||||
}
|
||||
return lexRvalue
|
||||
}
|
||||
|
||||
func init() {
|
||||
dateRegexp = regexp.MustCompile("^\\d{1,4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z")
|
||||
}
|
||||
|
||||
// Entry point
|
||||
func lex(input string) (*lexer, chan token) {
|
||||
l := &lexer{
|
||||
input: input,
|
||||
tokens: make(chan token),
|
||||
}
|
||||
go l.run()
|
||||
return l, l.tokens
|
||||
}
|
||||
-320
@@ -1,320 +0,0 @@
|
||||
package toml
|
||||
|
||||
import "testing"
|
||||
|
||||
func testFlow(t *testing.T, input string, expectedFlow []token) {
|
||||
_, ch := lex(input)
|
||||
for _, expected := range expectedFlow {
|
||||
token := <-ch
|
||||
if token != expected {
|
||||
t.Log("compared", token, "to", expected)
|
||||
t.Log(token.val, "<->", expected.val)
|
||||
t.Log(token.typ, "<->", expected.typ)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
tok, ok := <-ch
|
||||
if ok {
|
||||
t.Log("channel is not closed!")
|
||||
t.Log(len(ch)+1, "tokens remaining:")
|
||||
|
||||
t.Log("token ->", tok)
|
||||
for token := range ch {
|
||||
t.Log("token ->", token)
|
||||
}
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidKeyGroup(t *testing.T) {
|
||||
testFlow(t, "[hello world]", []token{
|
||||
token{tokenLeftBracket, "["},
|
||||
token{tokenKeyGroup, "hello world"},
|
||||
token{tokenRightBracket, "]"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnclosedKeyGroup(t *testing.T) {
|
||||
testFlow(t, "[hello world", []token{
|
||||
token{tokenLeftBracket, "["},
|
||||
token{tokenError, "unclosed key group"},
|
||||
})
|
||||
}
|
||||
|
||||
func TestComment(t *testing.T) {
|
||||
testFlow(t, "# blahblah", []token{
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyGroupComment(t *testing.T) {
|
||||
testFlow(t, "[hello world] # blahblah", []token{
|
||||
token{tokenLeftBracket, "["},
|
||||
token{tokenKeyGroup, "hello world"},
|
||||
token{tokenRightBracket, "]"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestMultipleKeyGroupsComment(t *testing.T) {
|
||||
testFlow(t, "[hello world] # blahblah\n[test]", []token{
|
||||
token{tokenLeftBracket, "["},
|
||||
token{tokenKeyGroup, "hello world"},
|
||||
token{tokenRightBracket, "]"},
|
||||
token{tokenLeftBracket, "["},
|
||||
token{tokenKeyGroup, "test"},
|
||||
token{tokenRightBracket, "]"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestBasicKey(t *testing.T) {
|
||||
testFlow(t, "hello", []token{
|
||||
token{tokenKey, "hello"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestBasicKeyWithUnderscore(t *testing.T) {
|
||||
testFlow(t, "hello_hello", []token{
|
||||
token{tokenKey, "hello_hello"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestBasicKeyWithUppercaseMix(t *testing.T) {
|
||||
testFlow(t, "helloHELLOHello", []token{
|
||||
token{tokenKey, "helloHELLOHello"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestBasicKeyWithInternationalCharacters(t *testing.T) {
|
||||
testFlow(t, "héllÖ", []token{
|
||||
token{tokenKey, "héllÖ"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestBasicKeyAndEqual(t *testing.T) {
|
||||
testFlow(t, "hello =", []token{
|
||||
token{tokenKey, "hello"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyEqualStringEscape(t *testing.T) {
|
||||
testFlow(t, "foo = \"hello\\\"\"", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenString, "hello\""},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyEqualStringUnfinished(t *testing.T) {
|
||||
testFlow(t, "foo = \"bar", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenError, "unclosed string"},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyEqualString(t *testing.T) {
|
||||
testFlow(t, "foo = \"bar\"", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenString, "bar"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyEqualTrue(t *testing.T) {
|
||||
testFlow(t, "foo = true", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenTrue, "true"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyEqualFalse(t *testing.T) {
|
||||
testFlow(t, "foo = false", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenFalse, "false"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayNestedString(t *testing.T) {
|
||||
testFlow(t, "a = [ [\"hello\", \"world\"] ]", []token{
|
||||
token{tokenKey, "a"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenLeftBracket, "["},
|
||||
token{tokenLeftBracket, "["},
|
||||
token{tokenString, "hello"},
|
||||
token{tokenComma, ","},
|
||||
token{tokenString, "world"},
|
||||
token{tokenRightBracket, "]"},
|
||||
token{tokenRightBracket, "]"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayNestedInts(t *testing.T) {
|
||||
testFlow(t, "a = [ [42, 21], [10] ]", []token{
|
||||
token{tokenKey, "a"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenLeftBracket, "["},
|
||||
token{tokenLeftBracket, "["},
|
||||
token{tokenInteger, "42"},
|
||||
token{tokenComma, ","},
|
||||
token{tokenInteger, "21"},
|
||||
token{tokenRightBracket, "]"},
|
||||
token{tokenComma, ","},
|
||||
token{tokenLeftBracket, "["},
|
||||
token{tokenInteger, "10"},
|
||||
token{tokenRightBracket, "]"},
|
||||
token{tokenRightBracket, "]"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayInts(t *testing.T) {
|
||||
testFlow(t, "a = [ 42, 21, 10, ]", []token{
|
||||
token{tokenKey, "a"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenLeftBracket, "["},
|
||||
token{tokenInteger, "42"},
|
||||
token{tokenComma, ","},
|
||||
token{tokenInteger, "21"},
|
||||
token{tokenComma, ","},
|
||||
token{tokenInteger, "10"},
|
||||
token{tokenComma, ","},
|
||||
token{tokenRightBracket, "]"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyEqualArrayBools(t *testing.T) {
|
||||
testFlow(t, "foo = [true, false, true]", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenLeftBracket, "["},
|
||||
token{tokenTrue, "true"},
|
||||
token{tokenComma, ","},
|
||||
token{tokenFalse, "false"},
|
||||
token{tokenComma, ","},
|
||||
token{tokenTrue, "true"},
|
||||
token{tokenRightBracket, "]"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyEqualArrayBoolsWithComments(t *testing.T) {
|
||||
testFlow(t, "foo = [true, false, true] # YEAH", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenLeftBracket, "["},
|
||||
token{tokenTrue, "true"},
|
||||
token{tokenComma, ","},
|
||||
token{tokenFalse, "false"},
|
||||
token{tokenComma, ","},
|
||||
token{tokenTrue, "true"},
|
||||
token{tokenRightBracket, "]"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestDateRegexp(t *testing.T) {
|
||||
if dateRegexp.FindString("1979-05-27T07:32:00Z") == "" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyEqualDate(t *testing.T) {
|
||||
testFlow(t, "foo = 1979-05-27T07:32:00Z", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenDate, "1979-05-27T07:32:00Z"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyEqualNumber(t *testing.T) {
|
||||
testFlow(t, "foo = 42", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenInteger, "42"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
|
||||
testFlow(t, "foo = +42", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenInteger, "+42"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
|
||||
testFlow(t, "foo = -42", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenInteger, "-42"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
|
||||
testFlow(t, "foo = 4.2", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenFloat, "4.2"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
|
||||
testFlow(t, "foo = +4.2", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenFloat, "+4.2"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
|
||||
testFlow(t, "foo = -4.2", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenFloat, "-4.2"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestMultiline(t *testing.T) {
|
||||
testFlow(t, "foo = 42\nbar=21", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenInteger, "42"},
|
||||
token{tokenKey, "bar"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenInteger, "21"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyEqualStringUnicodeEscape(t *testing.T) {
|
||||
testFlow(t, "foo = \"hello \\u2665\"", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenString, "hello ♥"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnicodeString(t *testing.T) {
|
||||
testFlow(t, "foo = \"hello ♥ world\"", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenString, "hello ♥ world"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LocalDate represents a calendar day in no specific timezone.
|
||||
type LocalDate struct {
|
||||
Year int
|
||||
Month int
|
||||
Day int
|
||||
}
|
||||
|
||||
// AsTime converts d into a specific time instance at midnight in zone.
|
||||
func (d LocalDate) AsTime(zone *time.Location) time.Time {
|
||||
return time.Date(d.Year, time.Month(d.Month), d.Day, 0, 0, 0, 0, zone)
|
||||
}
|
||||
|
||||
// String returns RFC 3339 representation of d.
|
||||
func (d LocalDate) String() string {
|
||||
return fmt.Sprintf("%04d-%02d-%02d", d.Year, d.Month, d.Day)
|
||||
}
|
||||
|
||||
// MarshalText returns RFC 3339 representation of d.
|
||||
func (d LocalDate) MarshalText() ([]byte, error) {
|
||||
return []byte(d.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalText parses b using RFC 3339 to fill d.
|
||||
func (d *LocalDate) UnmarshalText(b []byte) error {
|
||||
res, err := parseLocalDate(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*d = res
|
||||
return nil
|
||||
}
|
||||
|
||||
// LocalTime represents a time of day of no specific day in no specific
|
||||
// timezone.
|
||||
type LocalTime struct {
|
||||
Hour int // Hour of the day: [0; 24[
|
||||
Minute int // Minute of the hour: [0; 60[
|
||||
Second int // Second of the minute: [0; 60[
|
||||
Nanosecond int // Nanoseconds within the second: [0, 1000000000[
|
||||
Precision int // Number of digits to display for Nanosecond.
|
||||
}
|
||||
|
||||
// String returns RFC 3339 representation of d.
|
||||
// If d.Nanosecond and d.Precision are zero, the time won't have a nanosecond
|
||||
// component. If d.Nanosecond > 0 but d.Precision = 0, then the minimum number
|
||||
// of digits for nanoseconds is provided.
|
||||
func (d LocalTime) String() string {
|
||||
s := fmt.Sprintf("%02d:%02d:%02d", d.Hour, d.Minute, d.Second)
|
||||
|
||||
if d.Precision > 0 {
|
||||
s += fmt.Sprintf(".%09d", d.Nanosecond)[:d.Precision+1]
|
||||
} else if d.Nanosecond > 0 {
|
||||
// Nanoseconds are specified, but precision is not provided. Use the
|
||||
// minimum.
|
||||
s += strings.Trim(fmt.Sprintf(".%09d", d.Nanosecond), "0")
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// MarshalText returns RFC 3339 representation of d.
|
||||
func (d LocalTime) MarshalText() ([]byte, error) {
|
||||
return []byte(d.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalText parses b using RFC 3339 to fill d.
|
||||
func (d *LocalTime) UnmarshalText(b []byte) error {
|
||||
res, left, err := parseLocalTime(b)
|
||||
if err == nil && len(left) != 0 {
|
||||
err = newDecodeError(left, "extra characters")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*d = res
|
||||
return nil
|
||||
}
|
||||
|
||||
// LocalDateTime represents a time of a specific day in no specific timezone.
|
||||
type LocalDateTime struct {
|
||||
LocalDate
|
||||
LocalTime
|
||||
}
|
||||
|
||||
// AsTime converts d into a specific time instance in zone.
|
||||
func (d LocalDateTime) AsTime(zone *time.Location) time.Time {
|
||||
return time.Date(d.Year, time.Month(d.Month), d.Day, d.Hour, d.Minute, d.Second, d.Nanosecond, zone)
|
||||
}
|
||||
|
||||
// String returns RFC 3339 representation of d.
|
||||
func (d LocalDateTime) String() string {
|
||||
return d.LocalDate.String() + "T" + d.LocalTime.String()
|
||||
}
|
||||
|
||||
// MarshalText returns RFC 3339 representation of d.
|
||||
func (d LocalDateTime) MarshalText() ([]byte, error) {
|
||||
return []byte(d.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalText parses b using RFC 3339 to fill d.
|
||||
func (d *LocalDateTime) UnmarshalText(data []byte) error {
|
||||
res, left, err := parseLocalDateTime(data)
|
||||
if err == nil && len(left) != 0 {
|
||||
err = newDecodeError(left, "extra characters")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*d = res
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package toml_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLocalDate_AsTime(t *testing.T) {
|
||||
d := toml.LocalDate{2021, 6, 8}
|
||||
cast := d.AsTime(time.UTC)
|
||||
require.Equal(t, time.Date(2021, time.June, 8, 0, 0, 0, 0, time.UTC), cast)
|
||||
}
|
||||
|
||||
func TestLocalDate_String(t *testing.T) {
|
||||
d := toml.LocalDate{2021, 6, 8}
|
||||
require.Equal(t, "2021-06-08", d.String())
|
||||
}
|
||||
|
||||
func TestLocalDate_MarshalText(t *testing.T) {
|
||||
d := toml.LocalDate{2021, 6, 8}
|
||||
b, err := d.MarshalText()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []byte("2021-06-08"), b)
|
||||
}
|
||||
|
||||
func TestLocalDate_UnmarshalMarshalText(t *testing.T) {
|
||||
d := toml.LocalDate{}
|
||||
err := d.UnmarshalText([]byte("2021-06-08"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, toml.LocalDate{2021, 6, 8}, d)
|
||||
|
||||
err = d.UnmarshalText([]byte("what"))
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestLocalTime_String(t *testing.T) {
|
||||
d := toml.LocalTime{20, 12, 1, 2, 9}
|
||||
require.Equal(t, "20:12:01.000000002", d.String())
|
||||
d = toml.LocalTime{20, 12, 1, 0, 0}
|
||||
require.Equal(t, "20:12:01", d.String())
|
||||
d = toml.LocalTime{20, 12, 1, 0, 9}
|
||||
require.Equal(t, "20:12:01.000000000", d.String())
|
||||
d = toml.LocalTime{20, 12, 1, 100, 0}
|
||||
require.Equal(t, "20:12:01.0000001", d.String())
|
||||
}
|
||||
|
||||
func TestLocalTime_MarshalText(t *testing.T) {
|
||||
d := toml.LocalTime{20, 12, 1, 2, 9}
|
||||
b, err := d.MarshalText()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []byte("20:12:01.000000002"), b)
|
||||
}
|
||||
|
||||
func TestLocalTime_UnmarshalMarshalText(t *testing.T) {
|
||||
d := toml.LocalTime{}
|
||||
err := d.UnmarshalText([]byte("20:12:01.000000002"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, toml.LocalTime{20, 12, 1, 2, 9}, d)
|
||||
|
||||
err = d.UnmarshalText([]byte("what"))
|
||||
require.Error(t, err)
|
||||
|
||||
err = d.UnmarshalText([]byte("20:12:01.000000002 bad"))
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestLocalTime_RoundTrip(t *testing.T) {
|
||||
var d struct{ A toml.LocalTime }
|
||||
err := toml.Unmarshal([]byte("a=20:12:01.500"), &d)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "20:12:01.500", d.A.String())
|
||||
}
|
||||
|
||||
func TestLocalDateTime_AsTime(t *testing.T) {
|
||||
d := toml.LocalDateTime{
|
||||
toml.LocalDate{2021, 6, 8},
|
||||
toml.LocalTime{20, 12, 1, 2, 9},
|
||||
}
|
||||
cast := d.AsTime(time.UTC)
|
||||
require.Equal(t, time.Date(2021, time.June, 8, 20, 12, 1, 2, time.UTC), cast)
|
||||
}
|
||||
|
||||
func TestLocalDateTime_String(t *testing.T) {
|
||||
d := toml.LocalDateTime{
|
||||
toml.LocalDate{2021, 6, 8},
|
||||
toml.LocalTime{20, 12, 1, 2, 9},
|
||||
}
|
||||
require.Equal(t, "2021-06-08T20:12:01.000000002", d.String())
|
||||
}
|
||||
|
||||
func TestLocalDateTime_MarshalText(t *testing.T) {
|
||||
d := toml.LocalDateTime{
|
||||
toml.LocalDate{2021, 6, 8},
|
||||
toml.LocalTime{20, 12, 1, 2, 9},
|
||||
}
|
||||
b, err := d.MarshalText()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []byte("2021-06-08T20:12:01.000000002"), b)
|
||||
}
|
||||
|
||||
func TestLocalDateTime_UnmarshalMarshalText(t *testing.T) {
|
||||
d := toml.LocalDateTime{}
|
||||
err := d.UnmarshalText([]byte("2021-06-08 20:12:01.000000002"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, toml.LocalDateTime{
|
||||
toml.LocalDate{2021, 6, 8},
|
||||
toml.LocalTime{20, 12, 1, 2, 9},
|
||||
}, d)
|
||||
|
||||
err = d.UnmarshalText([]byte("what"))
|
||||
require.Error(t, err)
|
||||
|
||||
err = d.UnmarshalText([]byte("2021-06-08 20:12:01.000000002 bad"))
|
||||
require.Error(t, err)
|
||||
}
|
||||
+810
@@ -0,0 +1,810 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Marshal serializes a Go value as a TOML document.
|
||||
//
|
||||
// It is a shortcut for Encoder.Encode() with the default options.
|
||||
func Marshal(v interface{}) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
enc := NewEncoder(&buf)
|
||||
|
||||
err := enc.Encode(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// Encoder writes a TOML document to an output stream.
|
||||
type Encoder struct {
|
||||
// output
|
||||
w io.Writer
|
||||
|
||||
// global settings
|
||||
tablesInline bool
|
||||
arraysMultiline bool
|
||||
indentSymbol string
|
||||
indentTables bool
|
||||
}
|
||||
|
||||
// NewEncoder returns a new Encoder that writes to w.
|
||||
func NewEncoder(w io.Writer) *Encoder {
|
||||
return &Encoder{
|
||||
w: w,
|
||||
indentSymbol: " ",
|
||||
}
|
||||
}
|
||||
|
||||
// SetTablesInline forces the encoder to emit all tables inline.
|
||||
//
|
||||
// This behavior can be controlled on an individual struct field basis with the
|
||||
// inline tag:
|
||||
//
|
||||
// MyField `inline:"true"`
|
||||
func (enc *Encoder) SetTablesInline(inline bool) *Encoder {
|
||||
enc.tablesInline = inline
|
||||
return enc
|
||||
}
|
||||
|
||||
// SetArraysMultiline forces the encoder to emit all arrays with one element per
|
||||
// line.
|
||||
//
|
||||
// This behavior can be controlled on an individual struct field basis with the multiline tag:
|
||||
//
|
||||
// MyField `multiline:"true"`
|
||||
func (enc *Encoder) SetArraysMultiline(multiline bool) *Encoder {
|
||||
enc.arraysMultiline = multiline
|
||||
return enc
|
||||
}
|
||||
|
||||
// SetIndentSymbol defines the string that should be used for indentation. The
|
||||
// provided string is repeated for each indentation level. Defaults to two
|
||||
// spaces.
|
||||
func (enc *Encoder) SetIndentSymbol(s string) *Encoder {
|
||||
enc.indentSymbol = s
|
||||
return enc
|
||||
}
|
||||
|
||||
// SetIndentTables forces the encoder to intent tables and array tables.
|
||||
func (enc *Encoder) SetIndentTables(indent bool) *Encoder {
|
||||
enc.indentTables = indent
|
||||
return enc
|
||||
}
|
||||
|
||||
// Encode writes a TOML representation of v to the stream.
|
||||
//
|
||||
// If v cannot be represented to TOML it returns an error.
|
||||
//
|
||||
// Encoding rules
|
||||
//
|
||||
// A top level slice containing only maps or structs is encoded as [[table
|
||||
// array]].
|
||||
//
|
||||
// All slices not matching rule 1 are encoded as [array]. As a result, any map
|
||||
// or struct they contain is encoded as an {inline table}.
|
||||
//
|
||||
// Nil interfaces and nil pointers are not supported.
|
||||
//
|
||||
// Keys in key-values always have one part.
|
||||
//
|
||||
// Intermediate tables are always printed.
|
||||
//
|
||||
// By default, strings are encoded as literal string, unless they contain either
|
||||
// a newline character or a single quote. In that case they are emitted as quoted
|
||||
// strings.
|
||||
//
|
||||
// When encoding structs, fields are encoded in order of definition, with their
|
||||
// exact name.
|
||||
//
|
||||
// Struct tags
|
||||
//
|
||||
// The following struct tags are available to tweak encoding on a per-field
|
||||
// basis:
|
||||
//
|
||||
// toml:"foo"
|
||||
// Changes the name of the key to use for the field to foo.
|
||||
//
|
||||
// multiline:"true"
|
||||
// When the field contains a string, it will be emitted as a quoted
|
||||
// multi-line TOML string.
|
||||
//
|
||||
// inline:"true"
|
||||
// When the field would normally be encoded as a table, it is instead
|
||||
// encoded as an inline table.
|
||||
func (enc *Encoder) Encode(v interface{}) error {
|
||||
var (
|
||||
b []byte
|
||||
ctx encoderCtx
|
||||
)
|
||||
|
||||
ctx.inline = enc.tablesInline
|
||||
|
||||
if v == nil {
|
||||
return fmt.Errorf("toml: cannot encode a nil interface")
|
||||
}
|
||||
|
||||
b, err := enc.encode(b, ctx, reflect.ValueOf(v))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = enc.w.Write(b)
|
||||
if err != nil {
|
||||
return fmt.Errorf("toml: cannot write: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type valueOptions struct {
|
||||
multiline bool
|
||||
}
|
||||
|
||||
type encoderCtx struct {
|
||||
// Current top-level key.
|
||||
parentKey []string
|
||||
|
||||
// Key that should be used for a KV.
|
||||
key string
|
||||
// Extra flag to account for the empty string
|
||||
hasKey bool
|
||||
|
||||
// Set to true to indicate that the encoder is inside a KV, so that all
|
||||
// tables need to be inlined.
|
||||
insideKv bool
|
||||
|
||||
// Set to true to skip the first table header in an array table.
|
||||
skipTableHeader bool
|
||||
|
||||
// Should the next table be encoded as inline
|
||||
inline bool
|
||||
|
||||
// Indentation level
|
||||
indent int
|
||||
|
||||
// Options coming from struct tags
|
||||
options valueOptions
|
||||
}
|
||||
|
||||
func (ctx *encoderCtx) shiftKey() {
|
||||
if ctx.hasKey {
|
||||
ctx.parentKey = append(ctx.parentKey, ctx.key)
|
||||
ctx.clearKey()
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *encoderCtx) setKey(k string) {
|
||||
ctx.key = k
|
||||
ctx.hasKey = true
|
||||
}
|
||||
|
||||
func (ctx *encoderCtx) clearKey() {
|
||||
ctx.key = ""
|
||||
ctx.hasKey = false
|
||||
}
|
||||
|
||||
func (ctx *encoderCtx) isRoot() bool {
|
||||
return len(ctx.parentKey) == 0 && !ctx.hasKey
|
||||
}
|
||||
|
||||
//nolint:cyclop,funlen
|
||||
func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
|
||||
if !v.IsZero() {
|
||||
i, ok := v.Interface().(time.Time)
|
||||
if ok {
|
||||
return i.AppendFormat(b, time.RFC3339), nil
|
||||
}
|
||||
}
|
||||
|
||||
if v.Type().Implements(textMarshalerType) {
|
||||
if ctx.isRoot() {
|
||||
return nil, fmt.Errorf("toml: type %s implementing the TextMarshaler interface cannot be a root element", v.Type())
|
||||
}
|
||||
|
||||
text, err := v.Interface().(encoding.TextMarshaler).MarshalText()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b = enc.encodeString(b, string(text), ctx.options)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
// containers
|
||||
case reflect.Map:
|
||||
return enc.encodeMap(b, ctx, v)
|
||||
case reflect.Struct:
|
||||
return enc.encodeStruct(b, ctx, v)
|
||||
case reflect.Slice:
|
||||
return enc.encodeSlice(b, ctx, v)
|
||||
case reflect.Interface:
|
||||
if v.IsNil() {
|
||||
return nil, fmt.Errorf("toml: encoding a nil interface is not supported")
|
||||
}
|
||||
|
||||
return enc.encode(b, ctx, v.Elem())
|
||||
case reflect.Ptr:
|
||||
if v.IsNil() {
|
||||
return enc.encode(b, ctx, reflect.Zero(v.Type().Elem()))
|
||||
}
|
||||
|
||||
return enc.encode(b, ctx, v.Elem())
|
||||
|
||||
// values
|
||||
case reflect.String:
|
||||
b = enc.encodeString(b, v.String(), ctx.options)
|
||||
case reflect.Float32:
|
||||
if math.Trunc(v.Float()) == v.Float() {
|
||||
b = strconv.AppendFloat(b, v.Float(), 'f', 1, 32)
|
||||
} else {
|
||||
b = strconv.AppendFloat(b, v.Float(), 'f', -1, 32)
|
||||
}
|
||||
case reflect.Float64:
|
||||
if math.Trunc(v.Float()) == v.Float() {
|
||||
b = strconv.AppendFloat(b, v.Float(), 'f', 1, 64)
|
||||
} else {
|
||||
b = strconv.AppendFloat(b, v.Float(), 'f', -1, 64)
|
||||
}
|
||||
case reflect.Bool:
|
||||
if v.Bool() {
|
||||
b = append(b, "true"...)
|
||||
} else {
|
||||
b = append(b, "false"...)
|
||||
}
|
||||
case reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8, reflect.Uint:
|
||||
b = strconv.AppendUint(b, v.Uint(), 10)
|
||||
case reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int:
|
||||
b = strconv.AppendInt(b, v.Int(), 10)
|
||||
default:
|
||||
return nil, fmt.Errorf("toml: cannot encode value of type %s", v.Kind())
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func isNil(v reflect.Value) bool {
|
||||
switch v.Kind() {
|
||||
case reflect.Ptr, reflect.Interface, reflect.Map:
|
||||
return v.IsNil()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (enc *Encoder) encodeKv(b []byte, ctx encoderCtx, options valueOptions, v reflect.Value) ([]byte, error) {
|
||||
var err error
|
||||
|
||||
if !ctx.hasKey {
|
||||
panic("caller of encodeKv should have set the key in the context")
|
||||
}
|
||||
b = enc.indent(ctx.indent, b)
|
||||
|
||||
b, err = enc.encodeKey(b, ctx.key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b = append(b, " = "...)
|
||||
|
||||
// create a copy of the context because the value of a KV shouldn't
|
||||
// modify the global context.
|
||||
subctx := ctx
|
||||
subctx.insideKv = true
|
||||
subctx.shiftKey()
|
||||
subctx.options = options
|
||||
|
||||
b, err = enc.encode(b, subctx, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
const literalQuote = '\''
|
||||
|
||||
func (enc *Encoder) encodeString(b []byte, v string, options valueOptions) []byte {
|
||||
if needsQuoting(v) {
|
||||
return enc.encodeQuotedString(options.multiline, b, v)
|
||||
}
|
||||
|
||||
return enc.encodeLiteralString(b, v)
|
||||
}
|
||||
|
||||
func needsQuoting(v string) bool {
|
||||
return strings.ContainsAny(v, "'\b\f\n\r\t")
|
||||
}
|
||||
|
||||
// caller should have checked that the string does not contain new lines or ' .
|
||||
func (enc *Encoder) encodeLiteralString(b []byte, v string) []byte {
|
||||
b = append(b, literalQuote)
|
||||
b = append(b, v...)
|
||||
b = append(b, literalQuote)
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
//nolint:cyclop
|
||||
func (enc *Encoder) encodeQuotedString(multiline bool, b []byte, v string) []byte {
|
||||
stringQuote := `"`
|
||||
|
||||
if multiline {
|
||||
stringQuote = `"""`
|
||||
}
|
||||
|
||||
b = append(b, stringQuote...)
|
||||
if multiline {
|
||||
b = append(b, '\n')
|
||||
}
|
||||
|
||||
const (
|
||||
hextable = "0123456789ABCDEF"
|
||||
// U+0000 to U+0008, U+000A to U+001F, U+007F
|
||||
nul = 0x0
|
||||
bs = 0x8
|
||||
lf = 0xa
|
||||
us = 0x1f
|
||||
del = 0x7f
|
||||
)
|
||||
|
||||
for _, r := range []byte(v) {
|
||||
switch r {
|
||||
case '\\':
|
||||
b = append(b, `\\`...)
|
||||
case '"':
|
||||
b = append(b, `\"`...)
|
||||
case '\b':
|
||||
b = append(b, `\b`...)
|
||||
case '\f':
|
||||
b = append(b, `\f`...)
|
||||
case '\n':
|
||||
if multiline {
|
||||
b = append(b, r)
|
||||
} else {
|
||||
b = append(b, `\n`...)
|
||||
}
|
||||
case '\r':
|
||||
b = append(b, `\r`...)
|
||||
case '\t':
|
||||
b = append(b, `\t`...)
|
||||
default:
|
||||
switch {
|
||||
case r >= nul && r <= bs, r >= lf && r <= us, r == del:
|
||||
b = append(b, `\u00`...)
|
||||
b = append(b, hextable[r>>4])
|
||||
b = append(b, hextable[r&0x0f])
|
||||
default:
|
||||
b = append(b, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
b = append(b, stringQuote...)
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// called should have checked that the string is in A-Z / a-z / 0-9 / - / _ .
|
||||
func (enc *Encoder) encodeUnquotedKey(b []byte, v string) []byte {
|
||||
return append(b, v...)
|
||||
}
|
||||
|
||||
func (enc *Encoder) encodeTableHeader(ctx encoderCtx, b []byte) ([]byte, error) {
|
||||
if len(ctx.parentKey) == 0 {
|
||||
return b, nil
|
||||
}
|
||||
|
||||
b = enc.indent(ctx.indent, b)
|
||||
|
||||
b = append(b, '[')
|
||||
|
||||
var err error
|
||||
|
||||
b, err = enc.encodeKey(b, ctx.parentKey[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, k := range ctx.parentKey[1:] {
|
||||
b = append(b, '.')
|
||||
|
||||
b, err = enc.encodeKey(b, k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
b = append(b, "]\n"...)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
//nolint:cyclop
|
||||
func (enc *Encoder) encodeKey(b []byte, k string) ([]byte, error) {
|
||||
needsQuotation := false
|
||||
cannotUseLiteral := false
|
||||
|
||||
for _, c := range k {
|
||||
if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' {
|
||||
continue
|
||||
}
|
||||
|
||||
if c == '\n' {
|
||||
return nil, fmt.Errorf("toml: new line characters in keys are not supported")
|
||||
}
|
||||
|
||||
if c == literalQuote {
|
||||
cannotUseLiteral = true
|
||||
}
|
||||
|
||||
needsQuotation = true
|
||||
}
|
||||
|
||||
switch {
|
||||
case cannotUseLiteral:
|
||||
return enc.encodeQuotedString(false, b, k), nil
|
||||
case needsQuotation:
|
||||
return enc.encodeLiteralString(b, k), nil
|
||||
default:
|
||||
return enc.encodeUnquotedKey(b, k), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (enc *Encoder) encodeMap(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
|
||||
if v.Type().Key().Kind() != reflect.String {
|
||||
return nil, fmt.Errorf("toml: type %s is not supported as a map key", v.Type().Key().Kind())
|
||||
}
|
||||
|
||||
var (
|
||||
t table
|
||||
emptyValueOptions valueOptions
|
||||
)
|
||||
|
||||
iter := v.MapRange()
|
||||
for iter.Next() {
|
||||
k := iter.Key().String()
|
||||
v := iter.Value()
|
||||
|
||||
if isNil(v) {
|
||||
continue
|
||||
}
|
||||
|
||||
if willConvertToTableOrArrayTable(ctx, v) {
|
||||
t.pushTable(k, v, emptyValueOptions)
|
||||
} else {
|
||||
t.pushKV(k, v, emptyValueOptions)
|
||||
}
|
||||
}
|
||||
|
||||
sortEntriesByKey(t.kvs)
|
||||
sortEntriesByKey(t.tables)
|
||||
|
||||
return enc.encodeTable(b, ctx, t)
|
||||
}
|
||||
|
||||
func sortEntriesByKey(e []entry) {
|
||||
sort.Slice(e, func(i, j int) bool {
|
||||
return e[i].Key < e[j].Key
|
||||
})
|
||||
}
|
||||
|
||||
type entry struct {
|
||||
Key string
|
||||
Value reflect.Value
|
||||
Options valueOptions
|
||||
}
|
||||
|
||||
type table struct {
|
||||
kvs []entry
|
||||
tables []entry
|
||||
}
|
||||
|
||||
func (t *table) pushKV(k string, v reflect.Value, options valueOptions) {
|
||||
t.kvs = append(t.kvs, entry{Key: k, Value: v, Options: options})
|
||||
}
|
||||
|
||||
func (t *table) pushTable(k string, v reflect.Value, options valueOptions) {
|
||||
t.tables = append(t.tables, entry{Key: k, Value: v, Options: options})
|
||||
}
|
||||
|
||||
func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
|
||||
var t table
|
||||
|
||||
//nolint:godox
|
||||
// TODO: cache this?
|
||||
typ := v.Type()
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
fieldType := typ.Field(i)
|
||||
|
||||
// only consider exported fields
|
||||
if fieldType.PkgPath != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
k, ok := fieldType.Tag.Lookup("toml")
|
||||
if !ok {
|
||||
k = fieldType.Name
|
||||
}
|
||||
|
||||
// special field name to skip field
|
||||
if k == "-" {
|
||||
continue
|
||||
}
|
||||
|
||||
f := v.Field(i)
|
||||
|
||||
if isNil(f) {
|
||||
continue
|
||||
}
|
||||
|
||||
options := valueOptions{
|
||||
multiline: fieldBoolTag(fieldType, "multiline"),
|
||||
}
|
||||
|
||||
inline := fieldBoolTag(fieldType, "inline")
|
||||
|
||||
if inline || !willConvertToTableOrArrayTable(ctx, f) {
|
||||
t.pushKV(k, f, options)
|
||||
} else {
|
||||
t.pushTable(k, f, options)
|
||||
}
|
||||
}
|
||||
|
||||
return enc.encodeTable(b, ctx, t)
|
||||
}
|
||||
|
||||
func fieldBoolTag(field reflect.StructField, tag string) bool {
|
||||
x, ok := field.Tag.Lookup(tag)
|
||||
|
||||
return ok && x == "true"
|
||||
}
|
||||
|
||||
//nolint:cyclop
|
||||
func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, error) {
|
||||
var err error
|
||||
|
||||
ctx.shiftKey()
|
||||
|
||||
if ctx.insideKv || (ctx.inline && !ctx.isRoot()) {
|
||||
return enc.encodeTableInline(b, ctx, t)
|
||||
}
|
||||
|
||||
if !ctx.skipTableHeader {
|
||||
b, err = enc.encodeTableHeader(ctx, b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if enc.indentTables && len(ctx.parentKey) > 0 {
|
||||
ctx.indent++
|
||||
}
|
||||
}
|
||||
ctx.skipTableHeader = false
|
||||
|
||||
for _, kv := range t.kvs {
|
||||
ctx.setKey(kv.Key)
|
||||
|
||||
b, err = enc.encodeKv(b, ctx, kv.Options, kv.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b = append(b, '\n')
|
||||
}
|
||||
|
||||
for _, table := range t.tables {
|
||||
ctx.setKey(table.Key)
|
||||
|
||||
ctx.options = table.Options
|
||||
|
||||
b, err = enc.encode(b, ctx, table.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b = append(b, '\n')
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (enc *Encoder) encodeTableInline(b []byte, ctx encoderCtx, t table) ([]byte, error) {
|
||||
var err error
|
||||
|
||||
b = append(b, '{')
|
||||
|
||||
first := true
|
||||
for _, kv := range t.kvs {
|
||||
if first {
|
||||
first = false
|
||||
} else {
|
||||
b = append(b, `, `...)
|
||||
}
|
||||
|
||||
ctx.setKey(kv.Key)
|
||||
|
||||
b, err = enc.encodeKv(b, ctx, kv.Options, kv.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(t.tables) > 0 {
|
||||
panic("inline table cannot contain nested tables, online key-values")
|
||||
}
|
||||
|
||||
b = append(b, "}"...)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func willConvertToTable(ctx encoderCtx, v reflect.Value) bool {
|
||||
if !v.IsValid() {
|
||||
return false
|
||||
}
|
||||
if v.Type() == timeType || v.Type().Implements(textMarshalerType) {
|
||||
return false
|
||||
}
|
||||
|
||||
t := v.Type()
|
||||
switch t.Kind() {
|
||||
case reflect.Map, reflect.Struct:
|
||||
return !ctx.inline
|
||||
case reflect.Interface:
|
||||
return willConvertToTable(ctx, v.Elem())
|
||||
case reflect.Ptr:
|
||||
if v.IsNil() {
|
||||
return false
|
||||
}
|
||||
|
||||
return willConvertToTable(ctx, v.Elem())
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func willConvertToTableOrArrayTable(ctx encoderCtx, v reflect.Value) bool {
|
||||
t := v.Type()
|
||||
|
||||
if t.Kind() == reflect.Interface {
|
||||
return willConvertToTableOrArrayTable(ctx, v.Elem())
|
||||
}
|
||||
|
||||
if t.Kind() == reflect.Slice {
|
||||
if v.Len() == 0 {
|
||||
// An empty slice should be a kv = [].
|
||||
return false
|
||||
}
|
||||
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
t := willConvertToTable(ctx, v.Index(i))
|
||||
|
||||
if !t {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return willConvertToTable(ctx, v)
|
||||
}
|
||||
|
||||
func (enc *Encoder) encodeSlice(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
|
||||
if v.Len() == 0 {
|
||||
b = append(b, "[]"...)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
if willConvertToTableOrArrayTable(ctx, v) {
|
||||
return enc.encodeSliceAsArrayTable(b, ctx, v)
|
||||
}
|
||||
|
||||
return enc.encodeSliceAsArray(b, ctx, v)
|
||||
}
|
||||
|
||||
// caller should have checked that v is a slice that only contains values that
|
||||
// encode into tables.
|
||||
func (enc *Encoder) encodeSliceAsArrayTable(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
|
||||
ctx.shiftKey()
|
||||
|
||||
var err error
|
||||
scratch := make([]byte, 0, 64)
|
||||
scratch = append(scratch, "[["...)
|
||||
|
||||
for i, k := range ctx.parentKey {
|
||||
if i > 0 {
|
||||
scratch = append(scratch, '.')
|
||||
}
|
||||
|
||||
scratch, err = enc.encodeKey(scratch, k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
scratch = append(scratch, "]]\n"...)
|
||||
ctx.skipTableHeader = true
|
||||
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
b = append(b, scratch...)
|
||||
|
||||
b, err = enc.encode(b, ctx, v.Index(i))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (enc *Encoder) encodeSliceAsArray(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
|
||||
multiline := ctx.options.multiline || enc.arraysMultiline
|
||||
separator := ", "
|
||||
|
||||
b = append(b, '[')
|
||||
|
||||
subCtx := ctx
|
||||
subCtx.options = valueOptions{}
|
||||
|
||||
if multiline {
|
||||
separator = ",\n"
|
||||
|
||||
b = append(b, '\n')
|
||||
|
||||
subCtx.indent++
|
||||
}
|
||||
|
||||
var err error
|
||||
first := true
|
||||
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
if first {
|
||||
first = false
|
||||
} else {
|
||||
b = append(b, separator...)
|
||||
}
|
||||
|
||||
if multiline {
|
||||
b = enc.indent(subCtx.indent, b)
|
||||
}
|
||||
|
||||
b, err = enc.encode(b, subCtx, v.Index(i))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if multiline {
|
||||
b = append(b, '\n')
|
||||
b = enc.indent(ctx.indent, b)
|
||||
}
|
||||
|
||||
b = append(b, ']')
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (enc *Encoder) indent(level int, b []byte) []byte {
|
||||
for i := 0; i < level; i++ {
|
||||
b = append(b, enc.indentSymbol...)
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
@@ -0,0 +1,847 @@
|
||||
package toml_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
//nolint:funlen
|
||||
func TestMarshal(t *testing.T) {
|
||||
someInt := 42
|
||||
|
||||
type structInline struct {
|
||||
A interface{} `inline:"true"`
|
||||
}
|
||||
|
||||
examples := []struct {
|
||||
desc string
|
||||
v interface{}
|
||||
expected string
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
desc: "simple map and string",
|
||||
v: map[string]string{
|
||||
"hello": "world",
|
||||
},
|
||||
expected: "hello = 'world'",
|
||||
},
|
||||
{
|
||||
desc: "map with new line in key",
|
||||
v: map[string]string{
|
||||
"hel\nlo": "world",
|
||||
},
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
desc: `map with " in key`,
|
||||
v: map[string]string{
|
||||
`hel"lo`: "world",
|
||||
},
|
||||
expected: `'hel"lo' = 'world'`,
|
||||
},
|
||||
{
|
||||
desc: "map in map and string",
|
||||
v: map[string]map[string]string{
|
||||
"table": {
|
||||
"hello": "world",
|
||||
},
|
||||
},
|
||||
expected: `
|
||||
[table]
|
||||
hello = 'world'`,
|
||||
},
|
||||
{
|
||||
desc: "map in map in map and string",
|
||||
v: map[string]map[string]map[string]string{
|
||||
"this": {
|
||||
"is": {
|
||||
"a": "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: `
|
||||
[this]
|
||||
[this.is]
|
||||
a = 'test'`,
|
||||
},
|
||||
{
|
||||
desc: "map in map in map and string with values",
|
||||
v: map[string]interface{}{
|
||||
"this": map[string]interface{}{
|
||||
"is": map[string]string{
|
||||
"a": "test",
|
||||
},
|
||||
"also": "that",
|
||||
},
|
||||
},
|
||||
expected: `
|
||||
[this]
|
||||
also = 'that'
|
||||
[this.is]
|
||||
a = 'test'`,
|
||||
},
|
||||
{
|
||||
desc: "simple string array",
|
||||
v: map[string][]string{
|
||||
"array": {"one", "two", "three"},
|
||||
},
|
||||
expected: `array = ['one', 'two', 'three']`,
|
||||
},
|
||||
{
|
||||
desc: "empty string array",
|
||||
v: map[string][]string{},
|
||||
expected: ``,
|
||||
},
|
||||
{
|
||||
desc: "map",
|
||||
v: map[string][]string{},
|
||||
expected: ``,
|
||||
},
|
||||
{
|
||||
desc: "nested string arrays",
|
||||
v: map[string][][]string{
|
||||
"array": {{"one", "two"}, {"three"}},
|
||||
},
|
||||
expected: `array = [['one', 'two'], ['three']]`,
|
||||
},
|
||||
{
|
||||
desc: "mixed strings and nested string arrays",
|
||||
v: map[string][]interface{}{
|
||||
"array": {"a string", []string{"one", "two"}, "last"},
|
||||
},
|
||||
expected: `array = ['a string', ['one', 'two'], 'last']`,
|
||||
},
|
||||
{
|
||||
desc: "array of maps",
|
||||
v: map[string][]map[string]string{
|
||||
"top": {
|
||||
{"map1.1": "v1.1"},
|
||||
{"map2.1": "v2.1"},
|
||||
},
|
||||
},
|
||||
expected: `
|
||||
[[top]]
|
||||
'map1.1' = 'v1.1'
|
||||
[[top]]
|
||||
'map2.1' = 'v2.1'
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "map with two keys",
|
||||
v: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
},
|
||||
expected: `
|
||||
key1 = 'value1'
|
||||
key2 = 'value2'`,
|
||||
},
|
||||
{
|
||||
desc: "simple struct",
|
||||
v: struct {
|
||||
A string
|
||||
}{
|
||||
A: "foo",
|
||||
},
|
||||
expected: `A = 'foo'`,
|
||||
},
|
||||
{
|
||||
desc: "one level of structs within structs",
|
||||
v: struct {
|
||||
A interface{}
|
||||
}{
|
||||
A: struct {
|
||||
K1 string
|
||||
K2 string
|
||||
}{
|
||||
K1: "v1",
|
||||
K2: "v2",
|
||||
},
|
||||
},
|
||||
expected: `
|
||||
[A]
|
||||
K1 = 'v1'
|
||||
K2 = 'v2'
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "structs in array with interfaces",
|
||||
v: map[string]interface{}{
|
||||
"root": map[string]interface{}{
|
||||
"nested": []interface{}{
|
||||
map[string]interface{}{"name": "Bob"},
|
||||
map[string]interface{}{"name": "Alice"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: `
|
||||
[root]
|
||||
[[root.nested]]
|
||||
name = 'Bob'
|
||||
[[root.nested]]
|
||||
name = 'Alice'
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "string escapes",
|
||||
v: map[string]interface{}{
|
||||
"a": `'"\`,
|
||||
},
|
||||
expected: `a = "'\"\\"`,
|
||||
},
|
||||
{
|
||||
desc: "string utf8 low",
|
||||
v: map[string]interface{}{
|
||||
"a": "'Ę",
|
||||
},
|
||||
expected: `a = "'Ę"`,
|
||||
},
|
||||
{
|
||||
desc: "string utf8 low 2",
|
||||
v: map[string]interface{}{
|
||||
"a": "'\u10A85",
|
||||
},
|
||||
expected: "a = \"'\u10A85\"",
|
||||
},
|
||||
{
|
||||
desc: "string utf8 low 2",
|
||||
v: map[string]interface{}{
|
||||
"a": "'\u10A85",
|
||||
},
|
||||
expected: "a = \"'\u10A85\"",
|
||||
},
|
||||
{
|
||||
desc: "emoji",
|
||||
v: map[string]interface{}{
|
||||
"a": "'😀",
|
||||
},
|
||||
expected: "a = \"'😀\"",
|
||||
},
|
||||
{
|
||||
desc: "control char",
|
||||
v: map[string]interface{}{
|
||||
"a": "'\u001A",
|
||||
},
|
||||
expected: `a = "'\u001A"`,
|
||||
},
|
||||
{
|
||||
desc: "multi-line string",
|
||||
v: map[string]interface{}{
|
||||
"a": "hello\nworld",
|
||||
},
|
||||
expected: `a = "hello\nworld"`,
|
||||
},
|
||||
{
|
||||
desc: "multi-line forced",
|
||||
v: struct {
|
||||
A string `multiline:"true"`
|
||||
}{
|
||||
A: "hello\nworld",
|
||||
},
|
||||
expected: `A = """
|
||||
hello
|
||||
world"""`,
|
||||
},
|
||||
{
|
||||
desc: "inline field",
|
||||
v: struct {
|
||||
A map[string]string `inline:"true"`
|
||||
B map[string]string
|
||||
}{
|
||||
A: map[string]string{
|
||||
"isinline": "yes",
|
||||
},
|
||||
B: map[string]string{
|
||||
"isinline": "no",
|
||||
},
|
||||
},
|
||||
expected: `
|
||||
A = {isinline = 'yes'}
|
||||
[B]
|
||||
isinline = 'no'
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "mutiline array int",
|
||||
v: struct {
|
||||
A []int `multiline:"true"`
|
||||
B []int
|
||||
}{
|
||||
A: []int{1, 2, 3, 4},
|
||||
B: []int{1, 2, 3, 4},
|
||||
},
|
||||
expected: `
|
||||
A = [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
]
|
||||
B = [1, 2, 3, 4]
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "mutiline array in array",
|
||||
v: struct {
|
||||
A [][]int `multiline:"true"`
|
||||
}{
|
||||
A: [][]int{{1, 2}, {3, 4}},
|
||||
},
|
||||
expected: `
|
||||
A = [
|
||||
[1, 2],
|
||||
[3, 4]
|
||||
]
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "nil interface not supported at root",
|
||||
v: nil,
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
desc: "nil interface not supported in slice",
|
||||
v: map[string]interface{}{
|
||||
"a": []interface{}{"a", nil, 2},
|
||||
},
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
desc: "nil pointer in slice uses zero value",
|
||||
v: struct {
|
||||
A []*int
|
||||
}{
|
||||
A: []*int{nil},
|
||||
},
|
||||
expected: `A = [0]`,
|
||||
},
|
||||
{
|
||||
desc: "nil pointer in slice uses zero value",
|
||||
v: struct {
|
||||
A []*int
|
||||
}{
|
||||
A: []*int{nil},
|
||||
},
|
||||
expected: `A = [0]`,
|
||||
},
|
||||
{
|
||||
desc: "pointer in slice",
|
||||
v: struct {
|
||||
A []*int
|
||||
}{
|
||||
A: []*int{&someInt},
|
||||
},
|
||||
expected: `A = [42]`,
|
||||
},
|
||||
{
|
||||
desc: "inline table in inline table",
|
||||
v: structInline{
|
||||
A: structInline{
|
||||
A: structInline{
|
||||
A: "hello",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: `A = {A = {A = 'hello'}}`,
|
||||
},
|
||||
{
|
||||
desc: "empty slice in map",
|
||||
v: map[string][]string{
|
||||
"a": {},
|
||||
},
|
||||
expected: `a = []`,
|
||||
},
|
||||
{
|
||||
desc: "map in slice",
|
||||
v: map[string][]map[string]string{
|
||||
"a": {{"hello": "world"}},
|
||||
},
|
||||
expected: `
|
||||
[[a]]
|
||||
hello = 'world'`,
|
||||
},
|
||||
{
|
||||
desc: "newline in map in slice",
|
||||
v: map[string][]map[string]string{
|
||||
"a\n": {{"hello": "world"}},
|
||||
},
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
desc: "newline in map in slice",
|
||||
v: map[string][]map[string]*customTextMarshaler{
|
||||
"a": {{"hello": &customTextMarshaler{1}}},
|
||||
},
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
desc: "empty slice of empty struct",
|
||||
v: struct {
|
||||
A []struct{}
|
||||
}{
|
||||
A: []struct{}{},
|
||||
},
|
||||
expected: `A = []`,
|
||||
},
|
||||
{
|
||||
desc: "nil field is ignored",
|
||||
v: struct {
|
||||
A interface{}
|
||||
}{
|
||||
A: nil,
|
||||
},
|
||||
expected: ``,
|
||||
},
|
||||
{
|
||||
desc: "private fields are ignored",
|
||||
v: struct {
|
||||
Public string
|
||||
private string
|
||||
}{
|
||||
Public: "shown",
|
||||
private: "hidden",
|
||||
},
|
||||
expected: `Public = 'shown'`,
|
||||
},
|
||||
{
|
||||
desc: "fields tagged - are ignored",
|
||||
v: struct {
|
||||
Public string `toml:"-"`
|
||||
private string
|
||||
}{
|
||||
Public: "hidden",
|
||||
},
|
||||
expected: ``,
|
||||
},
|
||||
{
|
||||
desc: "nil value in map is ignored",
|
||||
v: map[string]interface{}{
|
||||
"A": nil,
|
||||
},
|
||||
expected: ``,
|
||||
},
|
||||
{
|
||||
desc: "new line in table key",
|
||||
v: map[string]interface{}{
|
||||
"hello\nworld": 42,
|
||||
},
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
desc: "new line in parent of nested table key",
|
||||
v: map[string]interface{}{
|
||||
"hello\nworld": map[string]interface{}{
|
||||
"inner": 42,
|
||||
},
|
||||
},
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
desc: "new line in nested table key",
|
||||
v: map[string]interface{}{
|
||||
"parent": map[string]interface{}{
|
||||
"in\ner": map[string]interface{}{
|
||||
"foo": 42,
|
||||
},
|
||||
},
|
||||
},
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid map key",
|
||||
v: map[int]interface{}{},
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
desc: "unhandled type",
|
||||
v: struct {
|
||||
A chan int
|
||||
}{
|
||||
A: make(chan int),
|
||||
},
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
desc: "numbers",
|
||||
v: struct {
|
||||
A float32
|
||||
B uint64
|
||||
C uint32
|
||||
D uint16
|
||||
E uint8
|
||||
F uint
|
||||
G int64
|
||||
H int32
|
||||
I int16
|
||||
J int8
|
||||
K int
|
||||
}{
|
||||
A: 1.1,
|
||||
B: 42,
|
||||
C: 42,
|
||||
D: 42,
|
||||
E: 42,
|
||||
F: 42,
|
||||
G: 42,
|
||||
H: 42,
|
||||
I: 42,
|
||||
J: 42,
|
||||
K: 42,
|
||||
},
|
||||
expected: `
|
||||
A = 1.1
|
||||
B = 42
|
||||
C = 42
|
||||
D = 42
|
||||
E = 42
|
||||
F = 42
|
||||
G = 42
|
||||
H = 42
|
||||
I = 42
|
||||
J = 42
|
||||
K = 42`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range examples {
|
||||
e := e
|
||||
t.Run(e.desc, func(t *testing.T) {
|
||||
b, err := toml.Marshal(e.v)
|
||||
if e.err {
|
||||
require.Error(t, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
equalStringsIgnoreNewlines(t, e.expected, string(b))
|
||||
|
||||
// make sure the output is always valid TOML
|
||||
defaultMap := map[string]interface{}{}
|
||||
err = toml.Unmarshal(b, &defaultMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
testWithAllFlags(t, func(t *testing.T, flags int) {
|
||||
t.Helper()
|
||||
|
||||
var buf bytes.Buffer
|
||||
enc := toml.NewEncoder(&buf)
|
||||
setFlags(enc, flags)
|
||||
|
||||
err := enc.Encode(e.v)
|
||||
require.NoError(t, err)
|
||||
|
||||
inlineMap := map[string]interface{}{}
|
||||
err = toml.Unmarshal(buf.Bytes(), &inlineMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, defaultMap, inlineMap)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type flagsSetters []struct {
|
||||
name string
|
||||
f func(enc *toml.Encoder, flag bool) *toml.Encoder
|
||||
}
|
||||
|
||||
var allFlags = flagsSetters{
|
||||
{"arrays-multiline", (*toml.Encoder).SetArraysMultiline},
|
||||
{"tables-inline", (*toml.Encoder).SetTablesInline},
|
||||
{"indent-tables", (*toml.Encoder).SetIndentTables},
|
||||
}
|
||||
|
||||
func setFlags(enc *toml.Encoder, flags int) {
|
||||
for i := 0; i < len(allFlags); i++ {
|
||||
enabled := flags&1 > 0
|
||||
allFlags[i].f(enc, enabled)
|
||||
}
|
||||
}
|
||||
|
||||
func testWithAllFlags(t *testing.T, testfn func(t *testing.T, flags int)) {
|
||||
t.Helper()
|
||||
testWithFlags(t, 0, allFlags, testfn)
|
||||
}
|
||||
|
||||
func testWithFlags(t *testing.T, flags int, setters flagsSetters, testfn func(t *testing.T, flags int)) {
|
||||
t.Helper()
|
||||
|
||||
if len(setters) == 0 {
|
||||
testfn(t, flags)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
s := setters[0]
|
||||
|
||||
for _, enabled := range []bool{false, true} {
|
||||
name := fmt.Sprintf("%s=%t", s.name, enabled)
|
||||
newFlags := flags << 1
|
||||
|
||||
if enabled {
|
||||
newFlags++
|
||||
}
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
testWithFlags(t, newFlags, setters[1:], testfn)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func equalStringsIgnoreNewlines(t *testing.T, expected string, actual string) {
|
||||
t.Helper()
|
||||
cutset := "\n"
|
||||
assert.Equal(t, strings.Trim(expected, cutset), strings.Trim(actual, cutset))
|
||||
}
|
||||
|
||||
//nolint:funlen
|
||||
func TestMarshalIndentTables(t *testing.T) {
|
||||
examples := []struct {
|
||||
desc string
|
||||
v interface{}
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "one kv",
|
||||
v: map[string]interface{}{
|
||||
"foo": "bar",
|
||||
},
|
||||
expected: `foo = 'bar'`,
|
||||
},
|
||||
{
|
||||
desc: "one level table",
|
||||
v: map[string]map[string]string{
|
||||
"foo": {
|
||||
"one": "value1",
|
||||
"two": "value2",
|
||||
},
|
||||
},
|
||||
expected: `
|
||||
[foo]
|
||||
one = 'value1'
|
||||
two = 'value2'
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "two levels table",
|
||||
v: map[string]interface{}{
|
||||
"root": "value0",
|
||||
"level1": map[string]interface{}{
|
||||
"one": "value1",
|
||||
"level2": map[string]interface{}{
|
||||
"two": "value2",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: `
|
||||
root = 'value0'
|
||||
[level1]
|
||||
one = 'value1'
|
||||
[level1.level2]
|
||||
two = 'value2'
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range examples {
|
||||
e := e
|
||||
t.Run(e.desc, func(t *testing.T) {
|
||||
var buf strings.Builder
|
||||
enc := toml.NewEncoder(&buf)
|
||||
enc.SetIndentTables(true)
|
||||
err := enc.Encode(e.v)
|
||||
require.NoError(t, err)
|
||||
equalStringsIgnoreNewlines(t, e.expected, buf.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type customTextMarshaler struct {
|
||||
value int64
|
||||
}
|
||||
|
||||
func (c *customTextMarshaler) MarshalText() ([]byte, error) {
|
||||
if c.value == 1 {
|
||||
return nil, fmt.Errorf("cannot represent 1 because this is a silly test")
|
||||
}
|
||||
return []byte(fmt.Sprintf("::%d", c.value)), nil
|
||||
}
|
||||
|
||||
func TestMarshalTextMarshaler_NoRoot(t *testing.T) {
|
||||
c := customTextMarshaler{}
|
||||
_, err := toml.Marshal(&c)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMarshalTextMarshaler_Error(t *testing.T) {
|
||||
m := map[string]interface{}{"a": &customTextMarshaler{value: 1}}
|
||||
_, err := toml.Marshal(m)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMarshalTextMarshaler_ErrorInline(t *testing.T) {
|
||||
type s struct {
|
||||
A map[string]interface{} `inline:"true"`
|
||||
}
|
||||
|
||||
d := s{
|
||||
A: map[string]interface{}{"a": &customTextMarshaler{value: 1}},
|
||||
}
|
||||
|
||||
_, err := toml.Marshal(d)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMarshalTextMarshaler(t *testing.T) {
|
||||
m := map[string]interface{}{"a": &customTextMarshaler{value: 2}}
|
||||
r, err := toml.Marshal(m)
|
||||
require.NoError(t, err)
|
||||
equalStringsIgnoreNewlines(t, "a = '::2'", string(r))
|
||||
}
|
||||
|
||||
type brokenWriter struct{}
|
||||
|
||||
func (b *brokenWriter) Write([]byte) (int, error) {
|
||||
return 0, fmt.Errorf("dead")
|
||||
}
|
||||
|
||||
func TestEncodeToBrokenWriter(t *testing.T) {
|
||||
w := brokenWriter{}
|
||||
enc := toml.NewEncoder(&w)
|
||||
err := enc.Encode(map[string]string{"hello": "world"})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestEncoderSetIndentSymbol(t *testing.T) {
|
||||
var w strings.Builder
|
||||
enc := toml.NewEncoder(&w)
|
||||
enc.SetIndentTables(true)
|
||||
enc.SetIndentSymbol(">>>")
|
||||
err := enc.Encode(map[string]map[string]string{"parent": {"hello": "world"}})
|
||||
require.NoError(t, err)
|
||||
expected := `
|
||||
[parent]
|
||||
>>>hello = 'world'`
|
||||
equalStringsIgnoreNewlines(t, expected, w.String())
|
||||
}
|
||||
|
||||
func TestIssue436(t *testing.T) {
|
||||
data := []byte(`{"a": [ { "b": { "c": "d" } } ]}`)
|
||||
|
||||
var v interface{}
|
||||
err := json.Unmarshal(data, &v)
|
||||
require.NoError(t, err)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = toml.NewEncoder(&buf).Encode(v)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := `
|
||||
[[a]]
|
||||
[a.b]
|
||||
c = 'd'
|
||||
`
|
||||
equalStringsIgnoreNewlines(t, expected, buf.String())
|
||||
}
|
||||
|
||||
func TestIssue424(t *testing.T) {
|
||||
type Message1 struct {
|
||||
Text string
|
||||
}
|
||||
|
||||
type Message2 struct {
|
||||
Text string `multiline:"true"`
|
||||
}
|
||||
|
||||
msg1 := Message1{"Hello\\World"}
|
||||
msg2 := Message2{"Hello\\World"}
|
||||
|
||||
toml1, err := toml.Marshal(msg1)
|
||||
require.NoError(t, err)
|
||||
|
||||
toml2, err := toml.Marshal(msg2)
|
||||
require.NoError(t, err)
|
||||
|
||||
msg1parsed := Message1{}
|
||||
err = toml.Unmarshal(toml1, &msg1parsed)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, msg1, msg1parsed)
|
||||
|
||||
msg2parsed := Message2{}
|
||||
err = toml.Unmarshal(toml2, &msg2parsed)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, msg2, msg2parsed)
|
||||
}
|
||||
|
||||
func TestIssue567(t *testing.T) {
|
||||
var m map[string]interface{}
|
||||
err := toml.Unmarshal([]byte("A = 12:08:05"), &m)
|
||||
require.NoError(t, err)
|
||||
require.IsType(t, m["A"], toml.LocalTime{})
|
||||
}
|
||||
|
||||
func TestIssue590(t *testing.T) {
|
||||
type CustomType int
|
||||
var cfg struct {
|
||||
Option CustomType `toml:"option"`
|
||||
}
|
||||
err := toml.Unmarshal([]byte("option = 42"), &cfg)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func ExampleMarshal() {
|
||||
type MyConfig struct {
|
||||
Version int
|
||||
Name string
|
||||
Tags []string
|
||||
}
|
||||
|
||||
cfg := MyConfig{
|
||||
Version: 2,
|
||||
Name: "go-toml",
|
||||
Tags: []string{"go", "toml"},
|
||||
}
|
||||
|
||||
b, err := toml.Marshal(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(string(b))
|
||||
|
||||
// Output:
|
||||
// Version = 2
|
||||
// Name = 'go-toml'
|
||||
// Tags = ['go', 'toml']
|
||||
}
|
||||
|
||||
func TestIssue571(t *testing.T) {
|
||||
type Foo struct {
|
||||
Float32 float32
|
||||
Float64 float64
|
||||
}
|
||||
|
||||
const closeEnough = 1e-9
|
||||
|
||||
foo := Foo{
|
||||
Float32: 42,
|
||||
Float64: 43,
|
||||
}
|
||||
b, err := toml.Marshal(foo)
|
||||
require.NoError(t, err)
|
||||
|
||||
var foo2 Foo
|
||||
err = toml.Unmarshal(b, &foo2)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.InDelta(t, 42, foo2.Float32, closeEnough)
|
||||
assert.InDelta(t, 43, foo2.Float64, closeEnough)
|
||||
}
|
||||
+424
-173
@@ -1,199 +1,450 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pelletier/go-toml/v2/internal/ast"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func assertTree(t *testing.T, tree *TomlTree, err error, ref map[string]interface{}) {
|
||||
if err != nil {
|
||||
t.Error("Non-nil error:", err.Error())
|
||||
return
|
||||
//nolint:funlen
|
||||
func TestParser_AST_Numbers(t *testing.T) {
|
||||
examples := []struct {
|
||||
desc string
|
||||
input string
|
||||
kind ast.Kind
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
desc: "integer just digits",
|
||||
input: `1234`,
|
||||
kind: ast.Integer,
|
||||
},
|
||||
{
|
||||
desc: "integer zero",
|
||||
input: `0`,
|
||||
kind: ast.Integer,
|
||||
},
|
||||
{
|
||||
desc: "integer sign",
|
||||
input: `+99`,
|
||||
kind: ast.Integer,
|
||||
},
|
||||
{
|
||||
desc: "integer hex uppercase",
|
||||
input: `0xDEADBEEF`,
|
||||
kind: ast.Integer,
|
||||
},
|
||||
{
|
||||
desc: "integer hex lowercase",
|
||||
input: `0xdead_beef`,
|
||||
kind: ast.Integer,
|
||||
},
|
||||
{
|
||||
desc: "integer octal",
|
||||
input: `0o01234567`,
|
||||
kind: ast.Integer,
|
||||
},
|
||||
{
|
||||
desc: "integer binary",
|
||||
input: `0b11010110`,
|
||||
kind: ast.Integer,
|
||||
},
|
||||
{
|
||||
desc: "float zero",
|
||||
input: `0.0`,
|
||||
kind: ast.Float,
|
||||
},
|
||||
{
|
||||
desc: "float positive zero",
|
||||
input: `+0.0`,
|
||||
kind: ast.Float,
|
||||
},
|
||||
{
|
||||
desc: "float negative zero",
|
||||
input: `-0.0`,
|
||||
kind: ast.Float,
|
||||
},
|
||||
{
|
||||
desc: "float pi",
|
||||
input: `3.1415`,
|
||||
kind: ast.Float,
|
||||
},
|
||||
{
|
||||
desc: "float negative",
|
||||
input: `-0.01`,
|
||||
kind: ast.Float,
|
||||
},
|
||||
{
|
||||
desc: "float signed exponent",
|
||||
input: `5e+22`,
|
||||
kind: ast.Float,
|
||||
},
|
||||
{
|
||||
desc: "float exponent lowercase",
|
||||
input: `1e06`,
|
||||
kind: ast.Float,
|
||||
},
|
||||
{
|
||||
desc: "float exponent uppercase",
|
||||
input: `-2E-2`,
|
||||
kind: ast.Float,
|
||||
},
|
||||
{
|
||||
desc: "float fractional with exponent",
|
||||
input: `6.626e-34`,
|
||||
kind: ast.Float,
|
||||
},
|
||||
{
|
||||
desc: "float underscores",
|
||||
input: `224_617.445_991_228`,
|
||||
kind: ast.Float,
|
||||
},
|
||||
{
|
||||
desc: "inf",
|
||||
input: `inf`,
|
||||
kind: ast.Float,
|
||||
},
|
||||
{
|
||||
desc: "inf negative",
|
||||
input: `-inf`,
|
||||
kind: ast.Float,
|
||||
},
|
||||
{
|
||||
desc: "inf positive",
|
||||
input: `+inf`,
|
||||
kind: ast.Float,
|
||||
},
|
||||
{
|
||||
desc: "nan",
|
||||
input: `nan`,
|
||||
kind: ast.Float,
|
||||
},
|
||||
{
|
||||
desc: "nan negative",
|
||||
input: `-nan`,
|
||||
kind: ast.Float,
|
||||
},
|
||||
{
|
||||
desc: "nan positive",
|
||||
input: `+nan`,
|
||||
kind: ast.Float,
|
||||
},
|
||||
}
|
||||
for k, v := range ref {
|
||||
if fmt.Sprintf("%v", tree.Get(k)) != fmt.Sprintf("%v", v) {
|
||||
t.Log("was expecting", v, "at", k, "but got", tree.Get(k))
|
||||
t.Error()
|
||||
|
||||
for _, e := range examples {
|
||||
e := e
|
||||
t.Run(e.desc, func(t *testing.T) {
|
||||
p := parser{}
|
||||
p.Reset([]byte(`A = ` + e.input))
|
||||
p.NextExpression()
|
||||
err := p.Error()
|
||||
if e.err {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := astNode{
|
||||
Kind: ast.KeyValue,
|
||||
Children: []astNode{
|
||||
{Kind: e.kind, Data: []byte(e.input)},
|
||||
{Kind: ast.Key, Data: []byte(`A`)},
|
||||
},
|
||||
}
|
||||
compareNode(t, expected, p.Expression())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type (
|
||||
astNode struct {
|
||||
Kind ast.Kind
|
||||
Data []byte
|
||||
Children []astNode
|
||||
}
|
||||
)
|
||||
|
||||
func compareNode(t *testing.T, e astNode, n *ast.Node) {
|
||||
t.Helper()
|
||||
require.Equal(t, e.Kind, n.Kind)
|
||||
require.Equal(t, e.Data, n.Data)
|
||||
|
||||
compareIterator(t, e.Children, n.Children())
|
||||
}
|
||||
|
||||
func compareIterator(t *testing.T, expected []astNode, actual ast.Iterator) {
|
||||
t.Helper()
|
||||
idx := 0
|
||||
|
||||
for actual.Next() {
|
||||
n := actual.Node()
|
||||
|
||||
if idx >= len(expected) {
|
||||
t.Fatal("extra child in actual tree")
|
||||
}
|
||||
e := expected[idx]
|
||||
|
||||
compareNode(t, e, n)
|
||||
|
||||
idx++
|
||||
}
|
||||
|
||||
if idx < len(expected) {
|
||||
t.Fatal("missing children in actual", "idx =", idx, "expected =", len(expected))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateSubTree(t *testing.T) {
|
||||
tree := make(TomlTree)
|
||||
tree.createSubTree("a.b.c")
|
||||
tree.Set("a.b.c", 42)
|
||||
if tree.Get("a.b.c") != 42 {
|
||||
t.Fail()
|
||||
//nolint:funlen
|
||||
func TestParser_AST(t *testing.T) {
|
||||
examples := []struct {
|
||||
desc string
|
||||
input string
|
||||
ast astNode
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
desc: "simple string assignment",
|
||||
input: `A = "hello"`,
|
||||
ast: astNode{
|
||||
Kind: ast.KeyValue,
|
||||
Children: []astNode{
|
||||
{
|
||||
Kind: ast.String,
|
||||
Data: []byte(`hello`),
|
||||
},
|
||||
{
|
||||
Kind: ast.Key,
|
||||
Data: []byte(`A`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "simple bool assignment",
|
||||
input: `A = true`,
|
||||
ast: astNode{
|
||||
Kind: ast.KeyValue,
|
||||
Children: []astNode{
|
||||
{
|
||||
Kind: ast.Bool,
|
||||
Data: []byte(`true`),
|
||||
},
|
||||
{
|
||||
Kind: ast.Key,
|
||||
Data: []byte(`A`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "array of strings",
|
||||
input: `A = ["hello", ["world", "again"]]`,
|
||||
ast: astNode{
|
||||
Kind: ast.KeyValue,
|
||||
Children: []astNode{
|
||||
{
|
||||
Kind: ast.Array,
|
||||
Children: []astNode{
|
||||
{
|
||||
Kind: ast.String,
|
||||
Data: []byte(`hello`),
|
||||
},
|
||||
{
|
||||
Kind: ast.Array,
|
||||
Children: []astNode{
|
||||
{
|
||||
Kind: ast.String,
|
||||
Data: []byte(`world`),
|
||||
},
|
||||
{
|
||||
Kind: ast.String,
|
||||
Data: []byte(`again`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: ast.Key,
|
||||
Data: []byte(`A`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "array of arrays of strings",
|
||||
input: `A = ["hello", "world"]`,
|
||||
ast: astNode{
|
||||
Kind: ast.KeyValue,
|
||||
Children: []astNode{
|
||||
{
|
||||
Kind: ast.Array,
|
||||
Children: []astNode{
|
||||
{
|
||||
Kind: ast.String,
|
||||
Data: []byte(`hello`),
|
||||
},
|
||||
{
|
||||
Kind: ast.String,
|
||||
Data: []byte(`world`),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: ast.Key,
|
||||
Data: []byte(`A`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "inline table",
|
||||
input: `name = { first = "Tom", last = "Preston-Werner" }`,
|
||||
ast: astNode{
|
||||
Kind: ast.KeyValue,
|
||||
Children: []astNode{
|
||||
{
|
||||
Kind: ast.InlineTable,
|
||||
Children: []astNode{
|
||||
{
|
||||
Kind: ast.KeyValue,
|
||||
Children: []astNode{
|
||||
{Kind: ast.String, Data: []byte(`Tom`)},
|
||||
{Kind: ast.Key, Data: []byte(`first`)},
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: ast.KeyValue,
|
||||
Children: []astNode{
|
||||
{Kind: ast.String, Data: []byte(`Preston-Werner`)},
|
||||
{Kind: ast.Key, Data: []byte(`last`)},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: ast.Key,
|
||||
Data: []byte(`name`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range examples {
|
||||
e := e
|
||||
t.Run(e.desc, func(t *testing.T) {
|
||||
p := parser{}
|
||||
p.Reset([]byte(e.input))
|
||||
p.NextExpression()
|
||||
err := p.Error()
|
||||
if e.err {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
compareNode(t, e.ast, p.Expression())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimpleKV(t *testing.T) {
|
||||
tree, err := Load("a = 42")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": int64(42),
|
||||
})
|
||||
func BenchmarkParseBasicStringWithUnicode(b *testing.B) {
|
||||
p := &parser{}
|
||||
b.Run("4", func(b *testing.B) {
|
||||
input := []byte(`"\u1234\u5678\u9ABC\u1234\u5678\u9ABC"`)
|
||||
b.ReportAllocs()
|
||||
b.SetBytes(int64(len(input)))
|
||||
|
||||
tree, _ = Load("a = 42\nb = 21")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": int64(42),
|
||||
"b": int64(21),
|
||||
for i := 0; i < b.N; i++ {
|
||||
p.parseBasicString(input)
|
||||
}
|
||||
})
|
||||
b.Run("8", func(b *testing.B) {
|
||||
input := []byte(`"\u12345678\u9ABCDEF0\u12345678\u9ABCDEF0"`)
|
||||
b.ReportAllocs()
|
||||
b.SetBytes(int64(len(input)))
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
p.parseBasicString(input)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSimpleNumbers(t *testing.T) {
|
||||
tree, err := Load("a = +42\nb = -21\nc = +4.2\nd = -2.1")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": int64(42),
|
||||
"b": int64(-21),
|
||||
"c": float64(4.2),
|
||||
"d": float64(-2.1),
|
||||
})
|
||||
}
|
||||
func BenchmarkParseBasicStringsEasy(b *testing.B) {
|
||||
p := &parser{}
|
||||
|
||||
func TestSimpleDate(t *testing.T) {
|
||||
tree, err := Load("a = 1979-05-27T07:32:00Z")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": time.Date(1979, time.May, 27, 7, 32, 0, 0, time.UTC),
|
||||
})
|
||||
}
|
||||
for _, size := range []int{1, 4, 8, 16, 21} {
|
||||
b.Run(strconv.Itoa(size), func(b *testing.B) {
|
||||
input := []byte(`"` + strings.Repeat("A", size) + `"`)
|
||||
|
||||
func TestSimpleString(t *testing.T) {
|
||||
tree, err := Load("a = \"hello world\"")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": "hello world",
|
||||
})
|
||||
}
|
||||
b.ReportAllocs()
|
||||
b.SetBytes(int64(len(input)))
|
||||
|
||||
func TestStringEscapables(t *testing.T) {
|
||||
tree, err := Load("a = \"a \\n b\"")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": "a \n b",
|
||||
})
|
||||
|
||||
tree, err = Load("a = \"a \\t b\"")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": "a \t b",
|
||||
})
|
||||
|
||||
tree, err = Load("a = \"a \\r b\"")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": "a \r b",
|
||||
})
|
||||
|
||||
tree, err = Load("a = \"a \\\\ b\"")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": "a \\ b",
|
||||
})
|
||||
}
|
||||
|
||||
func TestBools(t *testing.T) {
|
||||
tree, err := Load("a = true\nb = false")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": true,
|
||||
"b": false,
|
||||
})
|
||||
}
|
||||
|
||||
func TestNestedKeys(t *testing.T) {
|
||||
tree, err := Load("[a.b.c]\nd = 42")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a.b.c.d": int64(42),
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayOne(t *testing.T) {
|
||||
tree, err := Load("a = [1]")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": []int64{int64(1)},
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayZero(t *testing.T) {
|
||||
tree, err := Load("a = []")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": []interface{}{},
|
||||
})
|
||||
}
|
||||
|
||||
func TestArraySimple(t *testing.T) {
|
||||
tree, err := Load("a = [42, 21, 10]")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": []int64{int64(42), int64(21), int64(10)},
|
||||
})
|
||||
|
||||
tree, _ = Load("a = [42, 21, 10,]")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": []int64{int64(42), int64(21), int64(10)},
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayMultiline(t *testing.T) {
|
||||
tree, err := Load("a = [42,\n21, 10,]")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": []int64{int64(42), int64(21), int64(10)},
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayNested(t *testing.T) {
|
||||
tree, err := Load("a = [[42, 21], [10]]")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": [][]int64{[]int64{int64(42), int64(21)}, []int64{int64(10)}},
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayNestedStrings(t *testing.T) {
|
||||
tree, err := Load("data = [ [\"gamma\", \"delta\"], [\"Foo\"] ]")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"data": [][]string{[]string{"gamma", "delta"}, []string{"Foo"}},
|
||||
})
|
||||
}
|
||||
|
||||
func TestMissingValue(t *testing.T) {
|
||||
_, err := Load("a = ")
|
||||
if err.Error() != "expecting a value" {
|
||||
t.Error("Bad error message:", err.Error())
|
||||
for i := 0; i < b.N; i++ {
|
||||
p.parseBasicString(input)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnterminatedArray(t *testing.T) {
|
||||
_, err := Load("a = [1,")
|
||||
if err.Error() != "unterminated array" {
|
||||
t.Error("Bad error message:", err.Error())
|
||||
func TestParser_AST_DateTimes(t *testing.T) {
|
||||
examples := []struct {
|
||||
desc string
|
||||
input string
|
||||
kind ast.Kind
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
desc: "offset-date-time with delim 'T' and UTC offset",
|
||||
input: `2021-07-21T12:08:05Z`,
|
||||
kind: ast.DateTime,
|
||||
},
|
||||
{
|
||||
desc: "offset-date-time with space delim and +8hours offset",
|
||||
input: `2021-07-21 12:08:05+08:00`,
|
||||
kind: ast.DateTime,
|
||||
},
|
||||
{
|
||||
desc: "local-date-time with nano second",
|
||||
input: `2021-07-21T12:08:05.666666666`,
|
||||
kind: ast.LocalDateTime,
|
||||
},
|
||||
{
|
||||
desc: "local-date-time",
|
||||
input: `2021-07-21T12:08:05`,
|
||||
kind: ast.LocalDateTime,
|
||||
},
|
||||
{
|
||||
desc: "local-date",
|
||||
input: `2021-07-21`,
|
||||
kind: ast.LocalDate,
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range examples {
|
||||
e := e
|
||||
t.Run(e.desc, func(t *testing.T) {
|
||||
p := parser{}
|
||||
p.Reset([]byte(`A = ` + e.input))
|
||||
p.NextExpression()
|
||||
err := p.Error()
|
||||
if e.err {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := astNode{
|
||||
Kind: ast.KeyValue,
|
||||
Children: []astNode{
|
||||
{Kind: e.kind, Data: []byte(e.input)},
|
||||
{Kind: ast.Key, Data: []byte(`A`)},
|
||||
},
|
||||
}
|
||||
compareNode(t, expected, p.Expression())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewlinesInArrays(t *testing.T) {
|
||||
tree, err := Load("a = [1,\n2,\n3]")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": []int64{int64(1), int64(2), int64(3)},
|
||||
})
|
||||
}
|
||||
|
||||
func TestMissingFile(t *testing.T) {
|
||||
_, err := LoadFile("foo.toml")
|
||||
if err.Error() != "open foo.toml: no such file or directory" {
|
||||
t.Error("Bad error message:", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFile(t *testing.T) {
|
||||
tree, err := LoadFile("example.toml")
|
||||
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"title": "TOML Example",
|
||||
"owner.name": "Tom Preston-Werner",
|
||||
"owner.organization": "GitHub",
|
||||
"owner.bio": "GitHub Cofounder & CEO\nLikes tater tots and beer.",
|
||||
"owner.dob": time.Date(1979, time.May, 27, 7, 32, 0, 0, time.UTC),
|
||||
"database.server": "192.168.1.1",
|
||||
"database.ports": []int64{8001, 8001, 8002},
|
||||
"database.connection_max": 5000,
|
||||
"database.enabled": true,
|
||||
"servers.alpha.ip": "10.0.0.1",
|
||||
"servers.alpha.dc": "eqdc10",
|
||||
"servers.beta.ip": "10.0.0.2",
|
||||
"servers.beta.dc": "eqdc10",
|
||||
"clients.data": []interface{}{[]string{"gamma", "delta"}, []int64{1, 2}},
|
||||
})
|
||||
}
|
||||
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
package toml
|
||||
|
||||
func scanFollows(b []byte, pattern string) bool {
|
||||
n := len(pattern)
|
||||
|
||||
return len(b) >= n && string(b[:n]) == pattern
|
||||
}
|
||||
|
||||
func scanFollowsMultilineBasicStringDelimiter(b []byte) bool {
|
||||
return scanFollows(b, `"""`)
|
||||
}
|
||||
|
||||
func scanFollowsMultilineLiteralStringDelimiter(b []byte) bool {
|
||||
return scanFollows(b, `'''`)
|
||||
}
|
||||
|
||||
func scanFollowsTrue(b []byte) bool {
|
||||
return scanFollows(b, `true`)
|
||||
}
|
||||
|
||||
func scanFollowsFalse(b []byte) bool {
|
||||
return scanFollows(b, `false`)
|
||||
}
|
||||
|
||||
func scanFollowsInf(b []byte) bool {
|
||||
return scanFollows(b, `inf`)
|
||||
}
|
||||
|
||||
func scanFollowsNan(b []byte) bool {
|
||||
return scanFollows(b, `nan`)
|
||||
}
|
||||
|
||||
func scanUnquotedKey(b []byte) ([]byte, []byte) {
|
||||
// unquoted-key = 1*( ALPHA / DIGIT / %x2D / %x5F ) ; A-Z / a-z / 0-9 / - / _
|
||||
for i := 0; i < len(b); i++ {
|
||||
if !isUnquotedKeyChar(b[i]) {
|
||||
return b[:i], b[i:]
|
||||
}
|
||||
}
|
||||
|
||||
return b, b[len(b):]
|
||||
}
|
||||
|
||||
func isUnquotedKeyChar(r byte) bool {
|
||||
return (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' || r == '_'
|
||||
}
|
||||
|
||||
func scanLiteralString(b []byte) ([]byte, []byte, error) {
|
||||
// literal-string = apostrophe *literal-char apostrophe
|
||||
// apostrophe = %x27 ; ' apostrophe
|
||||
// literal-char = %x09 / %x20-26 / %x28-7E / non-ascii
|
||||
for i := 1; i < len(b); {
|
||||
switch b[i] {
|
||||
case '\'':
|
||||
return b[:i+1], b[i+1:], nil
|
||||
case '\n':
|
||||
return nil, nil, newDecodeError(b[i:i+1], "literal strings cannot have new lines")
|
||||
}
|
||||
size := utf8ValidNext(b[i:])
|
||||
if size == 0 {
|
||||
return nil, nil, newDecodeError(b[i:i+1], "invalid character")
|
||||
}
|
||||
i += size
|
||||
}
|
||||
|
||||
return nil, nil, newDecodeError(b[len(b):], "unterminated literal string")
|
||||
}
|
||||
|
||||
func scanMultilineLiteralString(b []byte) ([]byte, []byte, error) {
|
||||
// ml-literal-string = ml-literal-string-delim [ newline ] ml-literal-body
|
||||
// ml-literal-string-delim
|
||||
// ml-literal-string-delim = 3apostrophe
|
||||
// ml-literal-body = *mll-content *( mll-quotes 1*mll-content ) [ mll-quotes ]
|
||||
//
|
||||
// mll-content = mll-char / newline
|
||||
// mll-char = %x09 / %x20-26 / %x28-7E / non-ascii
|
||||
// mll-quotes = 1*2apostrophe
|
||||
for i := 3; i < len(b); {
|
||||
if scanFollowsMultilineLiteralStringDelimiter(b[i:]) {
|
||||
i += 3
|
||||
|
||||
// At that point we found 3 apostrophe, and i is the
|
||||
// index of the byte after the third one. The scanner
|
||||
// needs to be eager, because there can be an extra 2
|
||||
// apostrophe that can be accepted at the end of the
|
||||
// string.
|
||||
|
||||
if i >= len(b) || b[i] != '\'' {
|
||||
return b[:i], b[i:], nil
|
||||
}
|
||||
i++
|
||||
|
||||
if i >= len(b) || b[i] != '\'' {
|
||||
return b[:i], b[i:], nil
|
||||
}
|
||||
i++
|
||||
|
||||
if i < len(b) && b[i] == '\'' {
|
||||
return nil, nil, newDecodeError(b[i-3:i+1], "''' not allowed in multiline literal string")
|
||||
}
|
||||
|
||||
return b[:i], b[i:], nil
|
||||
}
|
||||
size := utf8ValidNext(b[i:])
|
||||
if size == 0 {
|
||||
return nil, nil, newDecodeError(b[i:i+1], "invalid character")
|
||||
}
|
||||
i += size
|
||||
}
|
||||
|
||||
return nil, nil, newDecodeError(b[len(b):], `multiline literal string not terminated by '''`)
|
||||
}
|
||||
|
||||
func scanWindowsNewline(b []byte) ([]byte, []byte, error) {
|
||||
const lenCRLF = 2
|
||||
if len(b) < lenCRLF {
|
||||
return nil, nil, newDecodeError(b, "windows new line expected")
|
||||
}
|
||||
|
||||
if b[1] != '\n' {
|
||||
return nil, nil, newDecodeError(b, `windows new line should be \r\n`)
|
||||
}
|
||||
|
||||
return b[:lenCRLF], b[lenCRLF:], nil
|
||||
}
|
||||
|
||||
func scanWhitespace(b []byte) ([]byte, []byte) {
|
||||
for i := 0; i < len(b); i++ {
|
||||
switch b[i] {
|
||||
case ' ', '\t':
|
||||
continue
|
||||
default:
|
||||
return b[:i], b[i:]
|
||||
}
|
||||
}
|
||||
|
||||
return b, b[len(b):]
|
||||
}
|
||||
|
||||
//nolint:unparam
|
||||
func scanComment(b []byte) ([]byte, []byte, error) {
|
||||
// comment-start-symbol = %x23 ; #
|
||||
// non-ascii = %x80-D7FF / %xE000-10FFFF
|
||||
// non-eol = %x09 / %x20-7F / non-ascii
|
||||
//
|
||||
// comment = comment-start-symbol *non-eol
|
||||
|
||||
for i := 1; i < len(b); {
|
||||
if b[i] == '\n' {
|
||||
return b[:i], b[i:], nil
|
||||
}
|
||||
if b[i] == '\r' {
|
||||
if i+1 < len(b) && b[i+1] == '\n' {
|
||||
return b[:i+1], b[i+1:], nil
|
||||
}
|
||||
return nil, nil, newDecodeError(b[i:i+1], "invalid character in comment")
|
||||
}
|
||||
size := utf8ValidNext(b[i:])
|
||||
if size == 0 {
|
||||
return nil, nil, newDecodeError(b[i:i+1], "invalid character in comment")
|
||||
}
|
||||
|
||||
i += size
|
||||
}
|
||||
|
||||
return b, b[len(b):], nil
|
||||
}
|
||||
|
||||
func scanBasicString(b []byte) ([]byte, bool, []byte, error) {
|
||||
// basic-string = quotation-mark *basic-char quotation-mark
|
||||
// quotation-mark = %x22 ; "
|
||||
// basic-char = basic-unescaped / escaped
|
||||
// basic-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
|
||||
// escaped = escape escape-seq-char
|
||||
escaped := false
|
||||
i := 1
|
||||
|
||||
for ; i < len(b); i++ {
|
||||
switch b[i] {
|
||||
case '"':
|
||||
return b[:i+1], escaped, b[i+1:], nil
|
||||
case '\n', '\r':
|
||||
return nil, escaped, nil, newDecodeError(b[i:i+1], "basic strings cannot have new lines")
|
||||
case '\\':
|
||||
if len(b) < i+2 {
|
||||
return nil, escaped, nil, newDecodeError(b[i:i+1], "need a character after \\")
|
||||
}
|
||||
escaped = true
|
||||
i++ // skip the next character
|
||||
}
|
||||
}
|
||||
|
||||
return nil, escaped, nil, newDecodeError(b[len(b):], `basic string not terminated by "`)
|
||||
}
|
||||
|
||||
func scanMultilineBasicString(b []byte) ([]byte, bool, []byte, error) {
|
||||
// ml-basic-string = ml-basic-string-delim [ newline ] ml-basic-body
|
||||
// ml-basic-string-delim
|
||||
// ml-basic-string-delim = 3quotation-mark
|
||||
// ml-basic-body = *mlb-content *( mlb-quotes 1*mlb-content ) [ mlb-quotes ]
|
||||
//
|
||||
// mlb-content = mlb-char / newline / mlb-escaped-nl
|
||||
// mlb-char = mlb-unescaped / escaped
|
||||
// mlb-quotes = 1*2quotation-mark
|
||||
// mlb-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
|
||||
// mlb-escaped-nl = escape ws newline *( wschar / newline )
|
||||
|
||||
escaped := false
|
||||
i := 3
|
||||
|
||||
for ; i < len(b); i++ {
|
||||
switch b[i] {
|
||||
case '"':
|
||||
if scanFollowsMultilineBasicStringDelimiter(b[i:]) {
|
||||
i += 3
|
||||
|
||||
// At that point we found 3 apostrophe, and i is the
|
||||
// index of the byte after the third one. The scanner
|
||||
// needs to be eager, because there can be an extra 2
|
||||
// apostrophe that can be accepted at the end of the
|
||||
// string.
|
||||
|
||||
if i >= len(b) || b[i] != '"' {
|
||||
return b[:i], escaped, b[i:], nil
|
||||
}
|
||||
i++
|
||||
|
||||
if i >= len(b) || b[i] != '"' {
|
||||
return b[:i], escaped, b[i:], nil
|
||||
}
|
||||
i++
|
||||
|
||||
if i < len(b) && b[i] == '"' {
|
||||
return nil, escaped, nil, newDecodeError(b[i-3:i+1], `""" not allowed in multiline basic string`)
|
||||
}
|
||||
|
||||
return b[:i], escaped, b[i:], nil
|
||||
}
|
||||
case '\\':
|
||||
if len(b) < i+2 {
|
||||
return nil, escaped, nil, newDecodeError(b[len(b):], "need a character after \\")
|
||||
}
|
||||
escaped = true
|
||||
i++ // skip the next character
|
||||
}
|
||||
}
|
||||
|
||||
return nil, escaped, nil, newDecodeError(b[len(b):], `multiline basic string not terminated by """`)
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"github.com/pelletier/go-toml/v2/internal/ast"
|
||||
"github.com/pelletier/go-toml/v2/internal/danger"
|
||||
"github.com/pelletier/go-toml/v2/internal/tracker"
|
||||
)
|
||||
|
||||
type strict struct {
|
||||
Enabled bool
|
||||
|
||||
// Tracks the current key being processed.
|
||||
key tracker.KeyTracker
|
||||
|
||||
missing []decodeError
|
||||
}
|
||||
|
||||
func (s *strict) EnterTable(node *ast.Node) {
|
||||
if !s.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
s.key.UpdateTable(node)
|
||||
}
|
||||
|
||||
func (s *strict) EnterArrayTable(node *ast.Node) {
|
||||
if !s.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
s.key.UpdateArrayTable(node)
|
||||
}
|
||||
|
||||
func (s *strict) EnterKeyValue(node *ast.Node) {
|
||||
if !s.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
s.key.Push(node)
|
||||
}
|
||||
|
||||
func (s *strict) ExitKeyValue(node *ast.Node) {
|
||||
if !s.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
s.key.Pop(node)
|
||||
}
|
||||
|
||||
func (s *strict) MissingTable(node *ast.Node) {
|
||||
if !s.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
s.missing = append(s.missing, decodeError{
|
||||
highlight: keyLocation(node),
|
||||
message: "missing table",
|
||||
key: s.key.Key(),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *strict) MissingField(node *ast.Node) {
|
||||
if !s.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
s.missing = append(s.missing, decodeError{
|
||||
highlight: keyLocation(node),
|
||||
message: "missing field",
|
||||
key: s.key.Key(),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *strict) Error(doc []byte) error {
|
||||
if !s.Enabled || len(s.missing) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := &StrictMissingError{
|
||||
Errors: make([]DecodeError, 0, len(s.missing)),
|
||||
}
|
||||
|
||||
for _, derr := range s.missing {
|
||||
derr := derr
|
||||
err.Errors = append(err.Errors, *wrapDecodeError(doc, &derr))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func keyLocation(node *ast.Node) []byte {
|
||||
k := node.Key()
|
||||
|
||||
hasOne := k.Next()
|
||||
if !hasOne {
|
||||
panic("should not be called with empty key")
|
||||
}
|
||||
|
||||
start := k.Node().Data
|
||||
end := k.Node().Data
|
||||
|
||||
for k.Next() {
|
||||
end = k.Node().Data
|
||||
}
|
||||
|
||||
return danger.BytesRange(start, end)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package testsuite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
)
|
||||
|
||||
// addTag adds JSON tags to a data structure as expected by toml-test.
|
||||
func addTag(key string, tomlData interface{}) interface{} {
|
||||
// Switch on the data type.
|
||||
switch orig := tomlData.(type) {
|
||||
default:
|
||||
//return map[string]interface{}{}
|
||||
panic(fmt.Sprintf("Unknown type: %T", tomlData))
|
||||
|
||||
// A table: we don't need to add any tags, just recurse for every table
|
||||
// entry.
|
||||
case map[string]interface{}:
|
||||
typed := make(map[string]interface{}, len(orig))
|
||||
for k, v := range orig {
|
||||
typed[k] = addTag(k, v)
|
||||
}
|
||||
return typed
|
||||
|
||||
// An array: we don't need to add any tags, just recurse for every table
|
||||
// entry.
|
||||
case []map[string]interface{}:
|
||||
typed := make([]map[string]interface{}, len(orig))
|
||||
for i, v := range orig {
|
||||
typed[i] = addTag("", v).(map[string]interface{})
|
||||
}
|
||||
return typed
|
||||
case []interface{}:
|
||||
typed := make([]interface{}, len(orig))
|
||||
for i, v := range orig {
|
||||
typed[i] = addTag("", v)
|
||||
}
|
||||
return typed
|
||||
|
||||
// Datetime: tag as datetime.
|
||||
case toml.LocalTime:
|
||||
return tag("time-local", orig.String())
|
||||
case toml.LocalDate:
|
||||
return tag("date-local", orig.String())
|
||||
case toml.LocalDateTime:
|
||||
return tag("datetime-local", orig.String())
|
||||
case time.Time:
|
||||
return tag("datetime", orig.Format("2006-01-02T15:04:05.999999999Z07:00"))
|
||||
|
||||
// Tag primitive values: bool, string, int, and float64.
|
||||
case bool:
|
||||
return tag("bool", fmt.Sprintf("%v", orig))
|
||||
case string:
|
||||
return tag("string", orig)
|
||||
case int64:
|
||||
return tag("integer", fmt.Sprintf("%d", orig))
|
||||
case float64:
|
||||
// Special case for nan since NaN == NaN is false.
|
||||
if math.IsNaN(orig) {
|
||||
return tag("float", "nan")
|
||||
}
|
||||
return tag("float", fmt.Sprintf("%v", orig))
|
||||
}
|
||||
}
|
||||
|
||||
func tag(typeName string, data interface{}) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"type": typeName,
|
||||
"value": data,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package testsuite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func CmpJSON(t *testing.T, key string, want, have interface{}) {
|
||||
switch w := want.(type) {
|
||||
case map[string]interface{}:
|
||||
cmpJSONMaps(t, key, w, have)
|
||||
case []interface{}:
|
||||
cmpJSONArrays(t, key, w, have)
|
||||
default:
|
||||
t.Errorf(
|
||||
"Key '%s' in expected output should be a map or a list of maps, but it's a %T",
|
||||
key, want)
|
||||
}
|
||||
}
|
||||
|
||||
func cmpJSONMaps(t *testing.T, key string, want map[string]interface{}, have interface{}) {
|
||||
haveMap, ok := have.(map[string]interface{})
|
||||
if !ok {
|
||||
mismatch(t, key, "table", want, haveMap)
|
||||
return
|
||||
}
|
||||
|
||||
// Check to make sure both or neither are values.
|
||||
if isValue(want) && !isValue(haveMap) {
|
||||
t.Fatalf("Key '%s' is supposed to be a value, but the parser reports it as a table", key)
|
||||
}
|
||||
if !isValue(want) && isValue(haveMap) {
|
||||
t.Fatalf("Key '%s' is supposed to be a table, but the parser reports it as a value", key)
|
||||
}
|
||||
if isValue(want) && isValue(haveMap) {
|
||||
cmpJSONValues(t, key, want, haveMap)
|
||||
return
|
||||
}
|
||||
|
||||
// Check that the keys of each map are equivalent.
|
||||
for k := range want {
|
||||
if _, ok := haveMap[k]; !ok {
|
||||
bunk := kjoin(key, k)
|
||||
t.Fatalf("Could not find key '%s' in parser output.", bunk)
|
||||
}
|
||||
}
|
||||
for k := range haveMap {
|
||||
if _, ok := want[k]; !ok {
|
||||
bunk := kjoin(key, k)
|
||||
t.Fatalf("Could not find key '%s' in expected output.", bunk)
|
||||
}
|
||||
}
|
||||
|
||||
// Okay, now make sure that each value is equivalent.
|
||||
for k := range want {
|
||||
CmpJSON(t, kjoin(key, k), want[k], haveMap[k])
|
||||
}
|
||||
}
|
||||
|
||||
func cmpJSONArrays(t *testing.T, key string, want, have interface{}) {
|
||||
wantSlice, ok := want.([]interface{})
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("'value' should be a JSON array when 'type=array', but it is a %T", want))
|
||||
}
|
||||
|
||||
haveSlice, ok := have.([]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("Malformed output from your encoder: 'value' is not a JSON array: %T", have)
|
||||
}
|
||||
|
||||
if len(wantSlice) != len(haveSlice) {
|
||||
t.Fatalf("Array lengths differ for key '%s':\n"+
|
||||
" Expected: %d\n"+
|
||||
" Your encoder: %d",
|
||||
key, len(wantSlice), len(haveSlice))
|
||||
}
|
||||
for i := 0; i < len(wantSlice); i++ {
|
||||
CmpJSON(t, key, wantSlice[i], haveSlice[i])
|
||||
}
|
||||
}
|
||||
|
||||
func cmpJSONValues(t *testing.T, key string, want, have map[string]interface{}) {
|
||||
wantType, ok := want["type"].(string)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("'type' should be a string, but it is a %T", want["type"]))
|
||||
}
|
||||
|
||||
haveType, ok := have["type"].(string)
|
||||
if !ok {
|
||||
t.Fatalf("Malformed output from your encoder: 'type' is not a string: %T", have["type"])
|
||||
}
|
||||
|
||||
if wantType != haveType {
|
||||
valMismatch(t, key, wantType, haveType, want, have)
|
||||
}
|
||||
|
||||
// If this is an array, then we've got to do some work to check equality.
|
||||
if wantType == "array" {
|
||||
cmpJSONArrays(t, key, want, have)
|
||||
return
|
||||
}
|
||||
|
||||
// Atomic values are always strings
|
||||
wantVal, ok := want["value"].(string)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("'value' %v should be a string, but it is a %[1]T", want["value"]))
|
||||
}
|
||||
|
||||
haveVal, ok := have["value"].(string)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("Malformed output from your encoder: %T is not a string", have["value"]))
|
||||
}
|
||||
|
||||
// Excepting floats and datetimes, other values can be compared as strings.
|
||||
switch wantType {
|
||||
case "float":
|
||||
cmpFloats(t, key, wantVal, haveVal)
|
||||
case "datetime", "datetime-local", "date-local", "time-local":
|
||||
cmpAsDatetimes(t, key, wantType, wantVal, haveVal)
|
||||
default:
|
||||
cmpAsStrings(t, key, wantVal, haveVal)
|
||||
}
|
||||
}
|
||||
|
||||
func cmpAsStrings(t *testing.T, key string, want, have string) {
|
||||
if want != have {
|
||||
t.Fatalf("Values for key '%s' don't match:\n"+
|
||||
" Expected: %s\n"+
|
||||
" Your encoder: %s",
|
||||
key, want, have)
|
||||
}
|
||||
}
|
||||
|
||||
func cmpFloats(t *testing.T, key string, want, have string) {
|
||||
// Special case for NaN, since NaN != NaN.
|
||||
if strings.HasSuffix(want, "nan") || strings.HasSuffix(have, "nan") {
|
||||
if want != have {
|
||||
t.Fatalf("Values for key '%s' don't match:\n"+
|
||||
" Expected: %v\n"+
|
||||
" Your encoder: %v",
|
||||
key, want, have)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
wantF, err := strconv.ParseFloat(want, 64)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Could not read '%s' as a float value for key '%s'", want, key))
|
||||
}
|
||||
|
||||
haveF, err := strconv.ParseFloat(have, 64)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Malformed output from your encoder: key '%s' is not a float: '%s'", key, have))
|
||||
}
|
||||
|
||||
if wantF != haveF {
|
||||
t.Fatalf("Values for key '%s' don't match:\n"+
|
||||
" Expected: %v\n"+
|
||||
" Your encoder: %v",
|
||||
key, wantF, haveF)
|
||||
}
|
||||
}
|
||||
|
||||
var datetimeRepl = strings.NewReplacer(
|
||||
" ", "T",
|
||||
"t", "T",
|
||||
"z", "Z")
|
||||
|
||||
var layouts = map[string]string{
|
||||
"datetime": time.RFC3339Nano,
|
||||
"datetime-local": "2006-01-02T15:04:05.999999999",
|
||||
"date-local": "2006-01-02",
|
||||
"time-local": "15:04:05",
|
||||
}
|
||||
|
||||
func cmpAsDatetimes(t *testing.T, key string, kind, want, have string) {
|
||||
layout, ok := layouts[kind]
|
||||
if !ok {
|
||||
panic("should never happen")
|
||||
}
|
||||
|
||||
wantT, err := time.Parse(layout, datetimeRepl.Replace(want))
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Could not read '%s' as a datetime value for key '%s'", want, key))
|
||||
}
|
||||
|
||||
haveT, err := time.Parse(layout, datetimeRepl.Replace(want))
|
||||
if err != nil {
|
||||
t.Fatalf("Malformed output from your encoder: key '%s' is not a datetime: '%s'", key, have)
|
||||
return
|
||||
}
|
||||
if !wantT.Equal(haveT) {
|
||||
t.Fatalf("Values for key '%s' don't match:\n"+
|
||||
" Expected: %v\n"+
|
||||
" Your encoder: %v",
|
||||
key, wantT, haveT)
|
||||
}
|
||||
}
|
||||
|
||||
func cmpAsDatetimesLocal(t *testing.T, key string, want, have string) {
|
||||
if datetimeRepl.Replace(want) != datetimeRepl.Replace(have) {
|
||||
t.Fatalf("Values for key '%s' don't match:\n"+
|
||||
" Expected: %v\n"+
|
||||
" Your encoder: %v",
|
||||
key, want, have)
|
||||
}
|
||||
}
|
||||
|
||||
func kjoin(old, key string) string {
|
||||
if len(old) == 0 {
|
||||
return key
|
||||
}
|
||||
return old + "." + key
|
||||
}
|
||||
|
||||
func isValue(m map[string]interface{}) bool {
|
||||
if len(m) != 2 {
|
||||
return false
|
||||
}
|
||||
if _, ok := m["type"]; !ok {
|
||||
return false
|
||||
}
|
||||
if _, ok := m["value"]; !ok {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func mismatch(t *testing.T, key string, wantType string, want, have interface{}) {
|
||||
t.Fatalf("Key '%s' is not an %s but %[4]T:\n"+
|
||||
" Expected: %#[3]v\n"+
|
||||
" Your encoder: %#[4]v",
|
||||
key, wantType, want, have)
|
||||
}
|
||||
|
||||
func valMismatch(t *testing.T, key string, wantType, haveType string, want, have interface{}) {
|
||||
t.Fatalf("Key '%s' is not an %s but %s:\n"+
|
||||
" Expected: %#[3]v\n"+
|
||||
" Your encoder: %#[4]v",
|
||||
key, wantType, want, have)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package testsuite
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
)
|
||||
|
||||
type parser struct{}
|
||||
|
||||
func (p parser) Decode(input string) (output string, outputIsError bool, retErr error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
switch rr := r.(type) {
|
||||
case error:
|
||||
retErr = rr
|
||||
default:
|
||||
retErr = fmt.Errorf("%s", rr)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var v interface{}
|
||||
|
||||
if err := toml.Unmarshal([]byte(input), &v); err != nil {
|
||||
return err.Error(), true, nil
|
||||
}
|
||||
|
||||
j, err := json.MarshalIndent(addTag("", v), "", " ")
|
||||
if err != nil {
|
||||
return "", false, retErr
|
||||
}
|
||||
|
||||
return string(j), false, retErr
|
||||
}
|
||||
|
||||
func (p parser) Encode(input string) (output string, outputIsError bool, retErr error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
switch rr := r.(type) {
|
||||
case error:
|
||||
retErr = rr
|
||||
default:
|
||||
retErr = fmt.Errorf("%s", rr)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var tmp interface{}
|
||||
err := json.Unmarshal([]byte(input), &tmp)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
rm, err := rmTag(tmp)
|
||||
if err != nil {
|
||||
return err.Error(), true, retErr
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
err = toml.NewEncoder(buf).Encode(rm)
|
||||
if err != nil {
|
||||
return err.Error(), true, retErr
|
||||
}
|
||||
|
||||
return buf.String(), false, retErr
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package testsuite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Remove JSON tags to a data structure as returned by toml-test.
|
||||
func rmTag(typedJson interface{}) (interface{}, error) {
|
||||
// Check if key is in the table m.
|
||||
in := func(key string, m map[string]interface{}) bool {
|
||||
_, ok := m[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Switch on the data type.
|
||||
switch v := typedJson.(type) {
|
||||
|
||||
// Object: this can either be a TOML table or a primitive with tags.
|
||||
case map[string]interface{}:
|
||||
// This value represents a primitive: remove the tags and return just
|
||||
// the primitive value.
|
||||
if len(v) == 2 && in("type", v) && in("value", v) {
|
||||
ut, err := untag(v)
|
||||
if err != nil {
|
||||
return ut, fmt.Errorf("tag.Remove: %w", err)
|
||||
}
|
||||
return ut, nil
|
||||
}
|
||||
|
||||
// Table: remove tags on all children.
|
||||
m := make(map[string]interface{}, len(v))
|
||||
for k, v2 := range v {
|
||||
var err error
|
||||
m[k], err = rmTag(v2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
|
||||
// Array: remove tags from all itenm.
|
||||
case []interface{}:
|
||||
a := make([]interface{}, len(v))
|
||||
for i := range v {
|
||||
var err error
|
||||
a[i], err = rmTag(v[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// The top level must be an object or array.
|
||||
return nil, fmt.Errorf("unrecognized JSON format '%T'", typedJson)
|
||||
}
|
||||
|
||||
// Return a primitive: read the "type" and convert the "value" to that.
|
||||
func untag(typed map[string]interface{}) (interface{}, error) {
|
||||
t := typed["type"].(string)
|
||||
v := typed["value"].(string)
|
||||
switch t {
|
||||
case "string":
|
||||
return v, nil
|
||||
case "integer":
|
||||
n, err := strconv.ParseInt(v, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("untag: %w", err)
|
||||
}
|
||||
return n, nil
|
||||
case "float":
|
||||
f, err := strconv.ParseFloat(v, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("untag: %w", err)
|
||||
}
|
||||
return f, nil
|
||||
case "datetime":
|
||||
return parseTime(v, "2006-01-02T15:04:05.999999999Z07:00", false)
|
||||
case "datetime-local":
|
||||
return parseTime(v, "2006-01-02T15:04:05.999999999", true)
|
||||
case "date-local":
|
||||
return parseTime(v, "2006-01-02", true)
|
||||
case "time-local":
|
||||
return parseTime(v, "15:04:05.999999999", true)
|
||||
case "bool":
|
||||
switch v {
|
||||
case "true":
|
||||
return true, nil
|
||||
case "false":
|
||||
return false, nil
|
||||
}
|
||||
return nil, fmt.Errorf("untag: could not parse %q as a boolean", v)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("untag: unrecognized tag type %q", t)
|
||||
}
|
||||
|
||||
func parseTime(v, format string, local bool) (t time.Time, err error) {
|
||||
if local {
|
||||
t, err = time.ParseInLocation(format, v, time.Local)
|
||||
} else {
|
||||
t, err = time.Parse(format, v)
|
||||
}
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("Could not parse %q as a datetime: %w", v, err)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Package testsuite provides helper functions for interoperating with the
|
||||
// language-agnostic TOML test suite at github.com/BurntSushi/toml-test.
|
||||
package testsuite
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
)
|
||||
|
||||
// Marshal is a helpfer function for calling toml.Marshal
|
||||
//
|
||||
// Only needed to avoid package import loops.
|
||||
func Marshal(v interface{}) ([]byte, error) {
|
||||
return toml.Marshal(v)
|
||||
}
|
||||
|
||||
// Unmarshal is a helper function for calling toml.Unmarshal.
|
||||
//
|
||||
// Only needed to avoid package import loops.
|
||||
func Unmarshal(data []byte, v interface{}) error {
|
||||
return toml.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
// ValueToTaggedJSON takes a data structure and returns the tagged JSON
|
||||
// representation.
|
||||
func ValueToTaggedJSON(doc interface{}) ([]byte, error) {
|
||||
return json.MarshalIndent(addTag("", doc), "", " ")
|
||||
}
|
||||
|
||||
// DecodeStdin is a helper function for the toml-test binary interface. TOML input
|
||||
// is read from STDIN and a resulting tagged JSON representation is written to
|
||||
// STDOUT.
|
||||
func DecodeStdin() error {
|
||||
var decoded map[string]interface{}
|
||||
|
||||
if err := toml.NewDecoder(os.Stdin).Decode(&decoded); err != nil {
|
||||
return fmt.Errorf("Error decoding TOML: %s", err)
|
||||
}
|
||||
|
||||
j := json.NewEncoder(os.Stdout)
|
||||
j.SetIndent("", " ")
|
||||
if err := j.Encode(addTag("", decoded)); err != nil {
|
||||
return fmt.Errorf("Error encoding JSON: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
;; This document describes TOML's syntax, using the ABNF format (defined in
|
||||
;; RFC 5234 -- https://www.ietf.org/rfc/rfc5234.txt).
|
||||
;;
|
||||
;; All valid TOML documents will match this description, however certain
|
||||
;; invalid documents would need to be rejected as per the semantics described
|
||||
;; in the supporting text description.
|
||||
|
||||
;; It is possible to try this grammar interactively, using instaparse.
|
||||
;; http://instaparse.mojombo.com/
|
||||
;;
|
||||
;; To do so, in the lower right, click on Options and change `:input-format` to
|
||||
;; ':abnf'. Then paste this entire ABNF document into the grammar entry box
|
||||
;; (above the options). Then you can type or paste a sample TOML document into
|
||||
;; the beige box on the left. Tada!
|
||||
|
||||
;; Overall Structure
|
||||
|
||||
toml = expression *( newline expression )
|
||||
|
||||
expression = ws [ comment ]
|
||||
expression =/ ws keyval ws [ comment ]
|
||||
expression =/ ws table ws [ comment ]
|
||||
|
||||
;; Whitespace
|
||||
|
||||
ws = *wschar
|
||||
wschar = %x20 ; Space
|
||||
wschar =/ %x09 ; Horizontal tab
|
||||
|
||||
;; Newline
|
||||
|
||||
newline = %x0A ; LF
|
||||
newline =/ %x0D.0A ; CRLF
|
||||
|
||||
;; Comment
|
||||
|
||||
comment-start-symbol = %x23 ; #
|
||||
non-ascii = %x80-D7FF / %xE000-10FFFF
|
||||
non-eol = %x09 / %x20-7F / non-ascii
|
||||
|
||||
comment = comment-start-symbol *non-eol
|
||||
|
||||
;; Key-Value pairs
|
||||
|
||||
keyval = key keyval-sep val
|
||||
|
||||
key = simple-key / dotted-key
|
||||
simple-key = quoted-key / unquoted-key
|
||||
|
||||
unquoted-key = 1*( ALPHA / DIGIT / %x2D / %x5F ) ; A-Z / a-z / 0-9 / - / _
|
||||
quoted-key = basic-string / literal-string
|
||||
dotted-key = simple-key 1*( dot-sep simple-key )
|
||||
|
||||
dot-sep = ws %x2E ws ; . Period
|
||||
keyval-sep = ws %x3D ws ; =
|
||||
|
||||
val = string / boolean / array / inline-table / date-time / float / integer
|
||||
|
||||
;; String
|
||||
|
||||
string = ml-basic-string / basic-string / ml-literal-string / literal-string
|
||||
|
||||
;; Basic String
|
||||
|
||||
basic-string = quotation-mark *basic-char quotation-mark
|
||||
|
||||
quotation-mark = %x22 ; "
|
||||
|
||||
basic-char = basic-unescaped / escaped
|
||||
basic-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
|
||||
escaped = escape escape-seq-char
|
||||
|
||||
escape = %x5C ; \
|
||||
escape-seq-char = %x22 ; " quotation mark U+0022
|
||||
escape-seq-char =/ %x5C ; \ reverse solidus U+005C
|
||||
escape-seq-char =/ %x62 ; b backspace U+0008
|
||||
escape-seq-char =/ %x66 ; f form feed U+000C
|
||||
escape-seq-char =/ %x6E ; n line feed U+000A
|
||||
escape-seq-char =/ %x72 ; r carriage return U+000D
|
||||
escape-seq-char =/ %x74 ; t tab U+0009
|
||||
escape-seq-char =/ %x75 4HEXDIG ; uXXXX U+XXXX
|
||||
escape-seq-char =/ %x55 8HEXDIG ; UXXXXXXXX U+XXXXXXXX
|
||||
|
||||
;; Multiline Basic String
|
||||
|
||||
ml-basic-string = ml-basic-string-delim [ newline ] ml-basic-body
|
||||
ml-basic-string-delim
|
||||
ml-basic-string-delim = 3quotation-mark
|
||||
ml-basic-body = *mlb-content *( mlb-quotes 1*mlb-content ) [ mlb-quotes ]
|
||||
|
||||
mlb-content = mlb-char / newline / mlb-escaped-nl
|
||||
mlb-char = mlb-unescaped / escaped
|
||||
mlb-quotes = 1*2quotation-mark
|
||||
mlb-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
|
||||
mlb-escaped-nl = escape ws newline *( wschar / newline )
|
||||
|
||||
;; Literal String
|
||||
|
||||
literal-string = apostrophe *literal-char apostrophe
|
||||
|
||||
apostrophe = %x27 ; ' apostrophe
|
||||
|
||||
literal-char = %x09 / %x20-26 / %x28-7E / non-ascii
|
||||
|
||||
;; Multiline Literal String
|
||||
|
||||
ml-literal-string = ml-literal-string-delim [ newline ] ml-literal-body
|
||||
ml-literal-string-delim
|
||||
ml-literal-string-delim = 3apostrophe
|
||||
ml-literal-body = *mll-content *( mll-quotes 1*mll-content ) [ mll-quotes ]
|
||||
|
||||
mll-content = mll-char / newline
|
||||
mll-char = %x09 / %x20-26 / %x28-7E / non-ascii
|
||||
mll-quotes = 1*2apostrophe
|
||||
|
||||
;; Integer
|
||||
|
||||
integer = dec-int / hex-int / oct-int / bin-int
|
||||
|
||||
minus = %x2D ; -
|
||||
plus = %x2B ; +
|
||||
underscore = %x5F ; _
|
||||
digit1-9 = %x31-39 ; 1-9
|
||||
digit0-7 = %x30-37 ; 0-7
|
||||
digit0-1 = %x30-31 ; 0-1
|
||||
|
||||
hex-prefix = %x30.78 ; 0x
|
||||
oct-prefix = %x30.6F ; 0o
|
||||
bin-prefix = %x30.62 ; 0b
|
||||
|
||||
dec-int = [ minus / plus ] unsigned-dec-int
|
||||
unsigned-dec-int = DIGIT / digit1-9 1*( DIGIT / underscore DIGIT )
|
||||
|
||||
hex-int = hex-prefix HEXDIG *( HEXDIG / underscore HEXDIG )
|
||||
oct-int = oct-prefix digit0-7 *( digit0-7 / underscore digit0-7 )
|
||||
bin-int = bin-prefix digit0-1 *( digit0-1 / underscore digit0-1 )
|
||||
|
||||
;; Float
|
||||
|
||||
float = float-int-part ( exp / frac [ exp ] )
|
||||
float =/ special-float
|
||||
|
||||
float-int-part = dec-int
|
||||
frac = decimal-point zero-prefixable-int
|
||||
decimal-point = %x2E ; .
|
||||
zero-prefixable-int = DIGIT *( DIGIT / underscore DIGIT )
|
||||
|
||||
exp = "e" float-exp-part
|
||||
float-exp-part = [ minus / plus ] zero-prefixable-int
|
||||
|
||||
special-float = [ minus / plus ] ( inf / nan )
|
||||
inf = %x69.6e.66 ; inf
|
||||
nan = %x6e.61.6e ; nan
|
||||
|
||||
;; Boolean
|
||||
|
||||
boolean = true / false
|
||||
|
||||
true = %x74.72.75.65 ; true
|
||||
false = %x66.61.6C.73.65 ; false
|
||||
|
||||
;; Date and Time (as defined in RFC 3339)
|
||||
|
||||
date-time = offset-date-time / local-date-time / local-date / local-time
|
||||
|
||||
date-fullyear = 4DIGIT
|
||||
date-month = 2DIGIT ; 01-12
|
||||
date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on month/year
|
||||
time-delim = "T" / %x20 ; T, t, or space
|
||||
time-hour = 2DIGIT ; 00-23
|
||||
time-minute = 2DIGIT ; 00-59
|
||||
time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second rules
|
||||
time-secfrac = "." 1*DIGIT
|
||||
time-numoffset = ( "+" / "-" ) time-hour ":" time-minute
|
||||
time-offset = "Z" / time-numoffset
|
||||
|
||||
partial-time = time-hour ":" time-minute ":" time-second [ time-secfrac ]
|
||||
full-date = date-fullyear "-" date-month "-" date-mday
|
||||
full-time = partial-time time-offset
|
||||
|
||||
;; Offset Date-Time
|
||||
|
||||
offset-date-time = full-date time-delim full-time
|
||||
|
||||
;; Local Date-Time
|
||||
|
||||
local-date-time = full-date time-delim partial-time
|
||||
|
||||
;; Local Date
|
||||
|
||||
local-date = full-date
|
||||
|
||||
;; Local Time
|
||||
|
||||
local-time = partial-time
|
||||
|
||||
;; Array
|
||||
|
||||
array = array-open [ array-values ] ws-comment-newline array-close
|
||||
|
||||
array-open = %x5B ; [
|
||||
array-close = %x5D ; ]
|
||||
|
||||
array-values = ws-comment-newline val ws-comment-newline array-sep array-values
|
||||
array-values =/ ws-comment-newline val ws-comment-newline [ array-sep ]
|
||||
|
||||
array-sep = %x2C ; , Comma
|
||||
|
||||
ws-comment-newline = *( wschar / [ comment ] newline )
|
||||
|
||||
;; Table
|
||||
|
||||
table = std-table / array-table
|
||||
|
||||
;; Standard Table
|
||||
|
||||
std-table = std-table-open key std-table-close
|
||||
|
||||
std-table-open = %x5B ws ; [ Left square bracket
|
||||
std-table-close = ws %x5D ; ] Right square bracket
|
||||
|
||||
;; Inline Table
|
||||
|
||||
inline-table = inline-table-open [ inline-table-keyvals ] inline-table-close
|
||||
|
||||
inline-table-open = %x7B ws ; {
|
||||
inline-table-close = ws %x7D ; }
|
||||
inline-table-sep = ws %x2C ws ; , Comma
|
||||
|
||||
inline-table-keyvals = keyval [ inline-table-sep inline-table-keyvals ]
|
||||
|
||||
;; Array Table
|
||||
|
||||
array-table = array-table-open key array-table-close
|
||||
|
||||
array-table-open = %x5B.5B ws ; [[ Double left square bracket
|
||||
array-table-close = ws %x5D.5D ; ]] Double right square bracket
|
||||
|
||||
;; Built-in ABNF terms, reproduced here for clarity
|
||||
|
||||
ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
|
||||
DIGIT = %x30-39 ; 0-9
|
||||
HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
|
||||
@@ -1,114 +0,0 @@
|
||||
// TOML markup language parser.
|
||||
//
|
||||
// This version supports the specification as described in
|
||||
// https://github.com/mojombo/toml/tree/e3656ad493400895f4460f1244a25f8f8e31a32a
|
||||
package toml
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Definition of a TomlTree.
|
||||
// This is the result of the parsing of a TOML file.
|
||||
type TomlTree map[string]interface{}
|
||||
|
||||
// Keys returns the keys of the toplevel tree.
|
||||
// Warning: this is a costly operation.
|
||||
func (t *TomlTree) Keys() []string {
|
||||
keys := make([]string, 0)
|
||||
mp := (map[string]interface{})(*t)
|
||||
for k, _ := range mp {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// Get the value at key in the TomlTree.
|
||||
// Key is a dot-separated path (e.g. a.b.c).
|
||||
// Returns nil if the path does not exist in the tree.
|
||||
func (t *TomlTree) Get(key string) interface{} {
|
||||
subtree := t
|
||||
keys := strings.Split(key, ".")
|
||||
for _, intermediate_key := range keys[:len(keys)-1] {
|
||||
_, exists := (*subtree)[intermediate_key]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
subtree = (*subtree)[intermediate_key].(*TomlTree)
|
||||
}
|
||||
return (*subtree)[keys[len(keys)-1]]
|
||||
}
|
||||
|
||||
// Same as Get but with a default value
|
||||
func (t *TomlTree) GetDefault(key string, def interface{}) interface{} {
|
||||
val := t.Get(key)
|
||||
if val == nil {
|
||||
return def
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
// Set an element in the tree.
|
||||
// Key is a dot-separated path (e.g. a.b.c).
|
||||
// Creates all necessary intermediates trees, if needed.
|
||||
func (t *TomlTree) Set(key string, value interface{}) {
|
||||
subtree := t
|
||||
keys := strings.Split(key, ".")
|
||||
for _, intermediate_key := range keys[:len(keys)-1] {
|
||||
_, exists := (*subtree)[intermediate_key]
|
||||
if !exists {
|
||||
var new_tree TomlTree = make(TomlTree)
|
||||
(*subtree)[intermediate_key] = &new_tree
|
||||
}
|
||||
subtree = (*subtree)[intermediate_key].(*TomlTree)
|
||||
}
|
||||
(*subtree)[keys[len(keys)-1]] = value
|
||||
}
|
||||
|
||||
// createSubTree takes a tree and a key and create the necessary intermediate
|
||||
// subtrees to create a subtree at that point. In-place.
|
||||
//
|
||||
// e.g. passing a.b.c will create (assuming tree is empty) tree[a], tree[a][b]
|
||||
// and tree[a][b][c]
|
||||
func (t *TomlTree) createSubTree(key string) {
|
||||
subtree := t
|
||||
for _, intermediate_key := range strings.Split(key, ".") {
|
||||
_, exists := (*subtree)[intermediate_key]
|
||||
if !exists {
|
||||
var new_tree TomlTree = make(TomlTree)
|
||||
(*subtree)[intermediate_key] = &new_tree
|
||||
}
|
||||
subtree = ((*subtree)[intermediate_key]).(*TomlTree)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a TomlTree from a string.
|
||||
func Load(content string) (tree *TomlTree, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if _, ok := r.(runtime.Error); ok {
|
||||
panic(r)
|
||||
}
|
||||
err = errors.New(r.(string))
|
||||
}
|
||||
}()
|
||||
_, flow := lex(content)
|
||||
tree = parse(flow)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a TomlTree from a file.
|
||||
func LoadFile(path string) (tree *TomlTree, err error) {
|
||||
buff, ferr := ioutil.ReadFile(path)
|
||||
if ferr != nil {
|
||||
err = ferr
|
||||
} else {
|
||||
s := string(buff)
|
||||
tree, err = Load(s)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
package toml
|
||||
@@ -0,0 +1,58 @@
|
||||
//go:generate go run ./cmd/tomltestgen/main.go -o toml_testgen_test.go
|
||||
|
||||
// This is a support file for toml_testgen_test.go
|
||||
package toml_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/pelletier/go-toml/v2/testsuite"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testgenInvalid(t *testing.T, input string) {
|
||||
t.Helper()
|
||||
t.Logf("Input TOML:\n%s", input)
|
||||
|
||||
doc := map[string]interface{}{}
|
||||
err := testsuite.Unmarshal([]byte(input), &doc)
|
||||
|
||||
if err == nil {
|
||||
out, err := json.Marshal(doc)
|
||||
if err != nil {
|
||||
panic("could not marshal map to json")
|
||||
}
|
||||
t.Log("JSON output from unmarshal:", string(out))
|
||||
t.Fatalf("test did not fail")
|
||||
}
|
||||
}
|
||||
|
||||
func testgenValid(t *testing.T, input string, jsonRef string) {
|
||||
t.Helper()
|
||||
t.Logf("Input TOML:\n%s", input)
|
||||
|
||||
// TODO: change this to interface{}
|
||||
var doc map[string]interface{}
|
||||
|
||||
err := testsuite.Unmarshal([]byte(input), &doc)
|
||||
if err != nil {
|
||||
if de, ok := err.(*toml.DecodeError); ok {
|
||||
t.Logf("%s\n%s", err, de)
|
||||
}
|
||||
t.Fatalf("failed parsing toml: %s", err)
|
||||
}
|
||||
j, err := testsuite.ValueToTaggedJSON(doc)
|
||||
require.NoError(t, err)
|
||||
|
||||
var ref interface{}
|
||||
err = json.Unmarshal([]byte(jsonRef), &ref)
|
||||
require.NoError(t, err)
|
||||
|
||||
var actual interface{}
|
||||
err = json.Unmarshal([]byte(j), &actual)
|
||||
require.NoError(t, err)
|
||||
|
||||
testsuite.CmpJSON(t, "", ref, actual)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
var timeType = reflect.TypeOf(time.Time{})
|
||||
var textMarshalerType = reflect.TypeOf(new(encoding.TextMarshaler)).Elem()
|
||||
var textUnmarshalerType = reflect.TypeOf(new(encoding.TextUnmarshaler)).Elem()
|
||||
var mapStringInterfaceType = reflect.TypeOf(map[string]interface{}{})
|
||||
var sliceInterfaceType = reflect.TypeOf([]interface{}{})
|
||||
var stringType = reflect.TypeOf("")
|
||||
+1240
File diff suppressed because it is too large
Load Diff
+2893
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,240 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type utf8Err struct {
|
||||
Index int
|
||||
Size int
|
||||
}
|
||||
|
||||
func (u utf8Err) Zero() bool {
|
||||
return u.Size == 0
|
||||
}
|
||||
|
||||
// Verified that a given string is only made of valid UTF-8 characters allowed
|
||||
// by the TOML spec:
|
||||
//
|
||||
// Any Unicode character may be used except those that must be escaped:
|
||||
// quotation mark, backslash, and the control characters other than tab (U+0000
|
||||
// to U+0008, U+000A to U+001F, U+007F).
|
||||
//
|
||||
// It is a copy of the Go 1.17 utf8.Valid implementation, tweaked to exit early
|
||||
// when a character is not allowed.
|
||||
//
|
||||
// The returned utf8Err is Zero() if the string is valid, or contains the byte
|
||||
// index and size of the invalid character.
|
||||
//
|
||||
// quotation mark => already checked
|
||||
// backslash => already checked
|
||||
// 0-0x8 => invalid
|
||||
// 0x9 => tab, ok
|
||||
// 0xA - 0x1F => invalid
|
||||
// 0x7F => invalid
|
||||
func utf8TomlValidAlreadyEscaped(p []byte) (err utf8Err) {
|
||||
// Fast path. Check for and skip 8 bytes of ASCII characters per iteration.
|
||||
offset := 0
|
||||
for len(p) >= 8 {
|
||||
// Combining two 32 bit loads allows the same code to be used
|
||||
// for 32 and 64 bit platforms.
|
||||
// The compiler can generate a 32bit load for first32 and second32
|
||||
// on many platforms. See test/codegen/memcombine.go.
|
||||
first32 := uint32(p[0]) | uint32(p[1])<<8 | uint32(p[2])<<16 | uint32(p[3])<<24
|
||||
second32 := uint32(p[4]) | uint32(p[5])<<8 | uint32(p[6])<<16 | uint32(p[7])<<24
|
||||
if (first32|second32)&0x80808080 != 0 {
|
||||
// Found a non ASCII byte (>= RuneSelf).
|
||||
break
|
||||
}
|
||||
|
||||
for i, b := range p[:8] {
|
||||
if invalidAscii(b) {
|
||||
err.Index = offset + i
|
||||
err.Size = 1
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
p = p[8:]
|
||||
offset += 8
|
||||
}
|
||||
n := len(p)
|
||||
for i := 0; i < n; {
|
||||
pi := p[i]
|
||||
if pi < utf8.RuneSelf {
|
||||
if invalidAscii(pi) {
|
||||
err.Index = offset + i
|
||||
err.Size = 1
|
||||
return
|
||||
}
|
||||
i++
|
||||
continue
|
||||
}
|
||||
x := first[pi]
|
||||
if x == xx {
|
||||
// Illegal starter byte.
|
||||
err.Index = offset + i
|
||||
err.Size = 1
|
||||
return
|
||||
}
|
||||
size := int(x & 7)
|
||||
if i+size > n {
|
||||
// Short or invalid.
|
||||
err.Index = offset + i
|
||||
err.Size = n - i
|
||||
return
|
||||
}
|
||||
accept := acceptRanges[x>>4]
|
||||
if c := p[i+1]; c < accept.lo || accept.hi < c {
|
||||
err.Index = offset + i
|
||||
err.Size = 2
|
||||
return
|
||||
} else if size == 2 {
|
||||
} else if c := p[i+2]; c < locb || hicb < c {
|
||||
err.Index = offset + i
|
||||
err.Size = 3
|
||||
return
|
||||
} else if size == 3 {
|
||||
} else if c := p[i+3]; c < locb || hicb < c {
|
||||
err.Index = offset + i
|
||||
err.Size = 4
|
||||
return
|
||||
}
|
||||
i += size
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Return the size of the next rune if valid, 0 otherwise.
|
||||
func utf8ValidNext(p []byte) int {
|
||||
c := p[0]
|
||||
|
||||
if c < utf8.RuneSelf {
|
||||
if invalidAscii(c) {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
x := first[c]
|
||||
if x == xx {
|
||||
// Illegal starter byte.
|
||||
return 0
|
||||
}
|
||||
size := int(x & 7)
|
||||
if size > len(p) {
|
||||
// Short or invalid.
|
||||
return 0
|
||||
}
|
||||
accept := acceptRanges[x>>4]
|
||||
if c := p[1]; c < accept.lo || accept.hi < c {
|
||||
return 0
|
||||
} else if size == 2 {
|
||||
} else if c := p[2]; c < locb || hicb < c {
|
||||
return 0
|
||||
} else if size == 3 {
|
||||
} else if c := p[3]; c < locb || hicb < c {
|
||||
return 0
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
|
||||
var invalidAsciiTable = [256]bool{
|
||||
0x00: true,
|
||||
0x01: true,
|
||||
0x02: true,
|
||||
0x03: true,
|
||||
0x04: true,
|
||||
0x05: true,
|
||||
0x06: true,
|
||||
0x07: true,
|
||||
0x08: true,
|
||||
// 0x09 TAB
|
||||
// 0x0A LF
|
||||
0x0B: true,
|
||||
0x0C: true,
|
||||
// 0x0D CR
|
||||
0x0E: true,
|
||||
0x0F: true,
|
||||
0x10: true,
|
||||
0x11: true,
|
||||
0x12: true,
|
||||
0x13: true,
|
||||
0x14: true,
|
||||
0x15: true,
|
||||
0x16: true,
|
||||
0x17: true,
|
||||
0x18: true,
|
||||
0x19: true,
|
||||
0x1A: true,
|
||||
0x1B: true,
|
||||
0x1C: true,
|
||||
0x1D: true,
|
||||
0x1E: true,
|
||||
0x1F: true,
|
||||
// 0x20 - 0x7E Printable ASCII characters
|
||||
0x7F: true,
|
||||
}
|
||||
|
||||
func invalidAscii(b byte) bool {
|
||||
return invalidAsciiTable[b]
|
||||
}
|
||||
|
||||
// acceptRange gives the range of valid values for the second byte in a UTF-8
|
||||
// sequence.
|
||||
type acceptRange struct {
|
||||
lo uint8 // lowest value for second byte.
|
||||
hi uint8 // highest value for second byte.
|
||||
}
|
||||
|
||||
// acceptRanges has size 16 to avoid bounds checks in the code that uses it.
|
||||
var acceptRanges = [16]acceptRange{
|
||||
0: {locb, hicb},
|
||||
1: {0xA0, hicb},
|
||||
2: {locb, 0x9F},
|
||||
3: {0x90, hicb},
|
||||
4: {locb, 0x8F},
|
||||
}
|
||||
|
||||
// first is information about the first byte in a UTF-8 sequence.
|
||||
var first = [256]uint8{
|
||||
// 1 2 3 4 5 6 7 8 9 A B C D E F
|
||||
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x00-0x0F
|
||||
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x10-0x1F
|
||||
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x20-0x2F
|
||||
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x30-0x3F
|
||||
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x40-0x4F
|
||||
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x50-0x5F
|
||||
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x60-0x6F
|
||||
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x70-0x7F
|
||||
// 1 2 3 4 5 6 7 8 9 A B C D E F
|
||||
xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0x80-0x8F
|
||||
xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0x90-0x9F
|
||||
xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0xA0-0xAF
|
||||
xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0xB0-0xBF
|
||||
xx, xx, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, // 0xC0-0xCF
|
||||
s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, // 0xD0-0xDF
|
||||
s2, s3, s3, s3, s3, s3, s3, s3, s3, s3, s3, s3, s3, s4, s3, s3, // 0xE0-0xEF
|
||||
s5, s6, s6, s6, s7, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0xF0-0xFF
|
||||
}
|
||||
|
||||
const (
|
||||
// The default lowest and highest continuation byte.
|
||||
locb = 0b10000000
|
||||
hicb = 0b10111111
|
||||
|
||||
// These names of these constants are chosen to give nice alignment in the
|
||||
// table below. The first nibble is an index into acceptRanges or F for
|
||||
// special one-byte cases. The second nibble is the Rune length or the
|
||||
// Status for the special one-byte case.
|
||||
xx = 0xF1 // invalid: size 1
|
||||
as = 0xF0 // ASCII: size 1
|
||||
s1 = 0x02 // accept 0, size 2
|
||||
s2 = 0x13 // accept 1, size 3
|
||||
s3 = 0x03 // accept 0, size 3
|
||||
s4 = 0x23 // accept 2, size 3
|
||||
s5 = 0x34 // accept 3, size 4
|
||||
s6 = 0x04 // accept 0, size 4
|
||||
s7 = 0x44 // accept 4, size 4
|
||||
)
|
||||
Reference in New Issue
Block a user