├── .github ├── Dockerfile.staticlibs ├── docker-compose.yml └── workflows │ ├── lint.yml │ └── staticlibs.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CODEOWNERS ├── LICENSE ├── README.md ├── con4m.nimble ├── config.nims ├── files ├── bin │ ├── buildlibs.sh │ └── devmode.sh ├── c │ ├── con4m.h │ └── test.c ├── con4m.nim ├── con4m │ ├── builtins.nim │ ├── c42spec.nim │ ├── c4m │ │ ├── c4m-cmdline.c4m │ │ ├── compiler-config.c42spec │ │ └── getopts.c42spec │ ├── capi.nim │ ├── codegen.nim │ ├── components.nim │ ├── doc.nim │ ├── dollars.nim │ ├── errmsg.nim │ ├── eval.nim │ ├── getopts.nim │ ├── legacy.nim │ ├── lex.nim │ ├── otherlits.nim │ ├── params.nim │ ├── parse.nim │ ├── run.nim │ ├── spec.nim │ ├── st.nim │ ├── stack.nim │ ├── strcursor.nim │ ├── treecheck.nim │ ├── typecheck.nim │ └── types.nim ├── deps │ ├── lib │ │ ├── linux-amd64 │ │ │ ├── libc.a │ │ │ ├── libcmark-gfm-extensions.a │ │ │ ├── libcmark-gfm.a │ │ │ ├── libcrypt.a │ │ │ ├── libcrypto.a │ │ │ ├── libdl.a │ │ │ ├── libgumbo.a │ │ │ ├── libm.a │ │ │ ├── libpcre.a │ │ │ ├── libpthread.a │ │ │ ├── libresolv.a │ │ │ ├── librt.a │ │ │ ├── libssl.a │ │ │ ├── libutil.a │ │ │ └── libxnet.a │ │ ├── linux-arm64 │ │ │ ├── libc.a │ │ │ ├── libcrypt.a │ │ │ ├── libcrypto.a │ │ │ ├── libdl.a │ │ │ ├── libgumbo.a │ │ │ ├── libm.a │ │ │ ├── libpcre.a │ │ │ ├── libpthread.a │ │ │ ├── libresolv.a │ │ │ ├── librt.a │ │ │ ├── libssl.a │ │ │ ├── libutil.a │ │ │ └── libxnet.a │ │ ├── macosx-amd64 │ │ │ ├── libcrypto.a │ │ │ ├── libpcre.a │ │ │ └── libssl.a │ │ └── macosx-arm64 │ │ │ ├── libcmark-gfm-extensions.a │ │ │ ├── libcmark-gfm.a │ │ │ ├── libcrypto.a │ │ │ ├── libgumbo.a │ │ │ ├── libpcre.a │ │ │ └── libssl.a │ └── macos │ │ ├── amd64 │ │ ├── libcrypto.a │ │ └── libssl.a │ │ └── arm64 │ │ ├── libcrypto.a │ │ └── libssl.a └── help │ ├── about.txt │ ├── api.txt │ ├── builtins.txt │ ├── cmd.txt │ ├── con4m.txt │ ├── help.txt │ ├── installing.txt │ ├── overview.txt │ ├── reference.txt │ └── specs.txt └── tests ├── basics ├── t1-hello.c4m ├── t1-hello.kat ├── t2-grabbag.c4m ├── t2-grabbag.kat ├── t3-attr-basic.c4m ├── t3-attr-basic.kat ├── t4-enum.c4m ├── t4-enum.kat ├── t5-func.c4m ├── t5-func.kat ├── t6-dupe.c4m ├── t6-dupe.kat ├── t7-fwref.c4m ├── t7-fwref.kat ├── t8-attr-mem.c4m └── t8-attr-mem.kat ├── samibase.c4m ├── spec ├── s1-basic.c4m ├── s1-basic.kat ├── s2-sami.c4m └── s2-sami.kat ├── stack ├── 1 │ ├── 1.c4m │ ├── 2.c4m │ ├── 3.c4m │ └── 4.c4m └── 1.kat └── test.nim /.github/Dockerfile.staticlibs: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/crashappsec/nim:ubuntu-2.0.0 as nim 2 | 3 | # https://github.com/actions/runner/issues/2033 4 | RUN if which git; then git config --global --add safe.directory "*"; fi 5 | 6 | # deps for compiling static deps 7 | RUN apt-get update -y && \ 8 | apt-get install -y \ 9 | autoconf \ 10 | cmake \ 11 | file \ 12 | g++ \ 13 | gcc \ 14 | git \ 15 | m4 \ 16 | make \ 17 | && \ 18 | apt-get clean -y 19 | 20 | WORKDIR /con4m 21 | -------------------------------------------------------------------------------- /.github/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | staticlibs: 3 | build: 4 | context: .. 5 | dockerfile: .github/Dockerfile.staticlibs 6 | working_dir: /con4m 7 | volumes: 8 | - ..:/con4m 9 | - $HOME/.local/c0:/root/.local/c0 10 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | pre-commit: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout Code 11 | uses: actions/checkout@v3 12 | 13 | - name: Setup Python 14 | uses: actions/setup-python@v3 15 | 16 | - name: Run pre-commit 17 | uses: pre-commit/action@v3.0.0 18 | -------------------------------------------------------------------------------- /.github/workflows/staticlibs.yml: -------------------------------------------------------------------------------- 1 | name: staticlibs 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | 7 | jobs: 8 | buildlibs: 9 | runs-on: ${{ matrix.builder }} 10 | 11 | strategy: 12 | matrix: 13 | include: 14 | - builder: ubuntu-latest 15 | arch: amd64 16 | os: linux 17 | - builder: buildjet-2vcpu-ubuntu-2204-arm 18 | arch: arm64 19 | os: linux 20 | 21 | steps: 22 | - name: Checkout Code 23 | uses: actions/checkout@v3 24 | 25 | - name: Show Static Libs 26 | run: | 27 | set -x 28 | ls -la ~/.local/c0/libs/* || true 29 | ls -la files/deps/lib/* || true 30 | 31 | - name: Clean up existing libs 32 | run: | 33 | rm -rf files/deps/lib/* 34 | 35 | - name: Build Static Libs 36 | working-directory: ./.github 37 | run: | 38 | docker compose run --rm staticlibs nimble build 39 | 40 | - name: Show Static Libs 41 | run: | 42 | set -x 43 | ls -la ~/.local/c0/libs/* || true 44 | ls -la files/deps/lib/* || true 45 | 46 | - name: Save Artifact 47 | uses: actions/upload-artifact@v3 48 | if: always() 49 | with: 50 | name: ${{ matrix.os }}-${{ matrix.arch }} 51 | path: ~/.local/c0/libs/* 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | nimcache/ 2 | nimblecache/ 3 | htmldocs/ 4 | tests/t[^.]*[^m] 5 | tags 6 | *.dSYM 7 | /con4m 8 | files/con4m/parse 9 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: | 2 | (?x)^( 3 | docs/.* 4 | )$ 5 | 6 | repos: 7 | - repo: https://github.com/pre-commit/pre-commit-hooks 8 | rev: v4.4.0 9 | hooks: 10 | - id: check-json 11 | - id: check-yaml 12 | - id: check-merge-conflict 13 | - id: end-of-file-fixer 14 | - id: mixed-line-ending 15 | - id: trailing-whitespace 16 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @viega 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Con4m: Configuration and Far More 2 | 3 | Con4m makes it easy to give users rich configurability via config 4 | files and command line flags. You just have to write a spec to get 5 | your config file format and your command line flags / parsing. 6 | 7 | You can do all of your input validation either through Con4m's built 8 | in constraints, or through custom validation routines (themselves 9 | written in con4m). Your main program can skip all that logic, and just 10 | get (or set) fields through a simple API. 11 | 12 | You can also document all your fields, commands and flags in your 13 | spec, which is then all easily accessible at runtime, or export the 14 | whole thing to JSon. 15 | 16 | 17 | By the 1.0 release, Con4m will be easily available in Go, Python, C 18 | and Nim (in which it's written). Currently, we build both a 19 | command-line and libcon4m, so C and Nim are particularly easy. 20 | 21 | But we've got a long way to go before 1.0-- we have a long backlog of 22 | work that we only have been doing as needed for 23 | [Chalk](https://github.com/crashappsec/chalk); once we ship that (in 24 | mid Sept) we will spend significant time on it. 25 | 26 | 27 | ## Brief Overview 28 | To the typical user, Con4m looks like a normal config file, somewhere 29 | in the NginX family. But! power users get a statically typed Go-like 30 | language that seamlessly integrates, for any power user needs, but is 31 | tailored for configuration use cases (for instance, having built-in 32 | data types for things like dates, times, durations and sizes). But, 33 | Con4m can be invisible when people don't need the extra power. 34 | 35 | You just write your own configuration file that specifies: 36 | 37 | 1. What sections and fields your want your configuration file to have, 38 | and what properties you want them to have. 39 | 40 | 2. What command-line commands and arguments you want to accept, and 41 | the properties you want them to have. 42 | 43 | 3. Any other custom validation you want to do. 44 | 45 | Then, you just have to call con4m, passing your config, and then your 46 | user's configuration file. You'll then be able to query results, and 47 | if you like, change values, run callbacks, run additional config files 48 | in the same context (or in different contexts)... 49 | 50 | As an example, replicating the whole of the command-line parsing for 51 | Docker took me about two hours, most of which was copying from their 52 | docs. 53 | 54 | Con4m validates configuration files before loading them, even making 55 | sure the types and values all what YOU need them to be, if you 56 | provide a brief specification defining the schema you want for your 57 | config files. That validation can include common constraints, (like 58 | the value must be from a fixed list or must be in a particular range). 59 | There are also constraints for field dependencies, and the ability to 60 | write custom field validation. Basically, just write the spec for 61 | what you want to accept in your config file, in Con4m, naturally. 62 | 63 | You are in total control -- do you want command-line flags to take 64 | prescendence over env variables? Over the config file? Do you want 65 | users to be able to add their own environment variables to suit their 66 | own needs? It's all easy. 67 | 68 | Con4m also allows you to 'stack' configuration files. For instance, 69 | the app can load an internal default configuration hardcoded into the 70 | program, then layer a system-level config over it, then layer a local 71 | config on top. 72 | 73 | After the configuration file loads, you can call any user-defined 74 | functions provided, if your application might need feedback from the 75 | user after configuration loads. 76 | 77 | You can also create your own builtin functions to make available to 78 | users who use the scripting capabilities. Con4m currently offers over 79 | 100 builtins, which you can selectively disable if you so choose. We 80 | always drop privs before running, and you can easily make full audits 81 | available. 82 | 83 | ## Basic Example 84 | 85 | Let’s imagine the user has provided a config file like this: 86 | 87 | ```python 88 | use_color: false 89 | log_level: warn 90 | 91 | host localhost { 92 | ip: "127.0.0.1" 93 | port: 8080 94 | } 95 | host workstation { 96 | port: 8080 97 | if env("CUSTOM_VAR") != "" { 98 | ip: env("CUSTOM_VAR") 99 | } 100 | else { 101 | ip: "10.12.1.10" 102 | } 103 | } 104 | ``` 105 | 106 | In this example, the conditional runs when the config file is 107 | evaluated (if something needs to be evaluated dynamically, you can do 108 | that with a callback). 109 | 110 | Con4m provides a number of builtin functions like env(), which makes 111 | it easy for you to check environment variables, but also for your 112 | users to customize environment variables to suit their needs. You can 113 | easily provide your own built-in functions. 114 | 115 | Let’s say the application writer has loaded this configuration file 116 | into the variable `s`. She may then write the following Con4m spec: 117 | 118 | ```python 119 | object host { 120 | field ip { 121 | type: IPAddr 122 | require: true 123 | } 124 | 125 | field use_tls { 126 | type: bool 127 | default: true 128 | } 129 | } 130 | 131 | # Stick this in a hidden variable, we'll use it for the command line too. 132 | 133 | valid_log_levels := ["verbose", "info", "warn", "error", "none"] 134 | root { 135 | allow host {} 136 | field use_color { 137 | type: bool 138 | require: false 139 | } 140 | 141 | field log_level { 142 | choice: valid_log_levels 143 | default: "info" 144 | } 145 | } 146 | ``` 147 | 148 | When you load a user configuration file via Con4m, if you also pass it 149 | the above spec, the following will happen (in roughly this order): 150 | 151 | 1. The spec file loads and is validated. 152 | 1. The user's configuration is read in, and checked for syntax and type 153 | safety. 154 | 1. If the user skips attributes where you've provided a default, those 155 | values will be loaded from your spec before evaluation. If you 156 | didn't provide a value, but the field is required, then the user 157 | gets an appropriate error before the configuration is evaluated. 158 | 1. The user's config is evaluated. 159 | 1. The user's config file is checked against the constraints provided 160 | in the spec. You can also provide additional validation 161 | constraints, like forcing strings to be from a particular set of 162 | values, or having integers be in a range. Whenever these constraints 163 | are violated, the user gets a descriptive error message. 164 | 165 | Any doc strings you provide (per section or per field) are 166 | programatically available, and can be put into nice tables. 167 | 168 | You then get an easy API for querying and setting these values as your 169 | code runs. And, you can call back into the user's config via callback 170 | whenever needed. 171 | 172 | ## Command-line example 173 | If you wanted to provide command-line flags that could be used, and 174 | want them to take presidence over the configuration file. Bulding on the 175 | previous example you could do add the following: 176 | 177 | ```python 178 | ... 179 | 180 | getopts { 181 | flag_yn color { 182 | yes_aliases: ["c"] 183 | no_aliases: ["C"] 184 | field_to_set: "use_color" 185 | doc: "Enable colors (overriding any config file settings)" 186 | } 187 | flag_help { } # Automatically add help 188 | 189 | flag_choice log_level { 190 | aliases: ["l"] 191 | choices: valid_log_levels 192 | add_choice_flags: true 193 | field_to_set: "log_level" 194 | } 195 | 196 | ... 197 | 198 | command run { 199 | aliases: ["r"] 200 | args: (0, high()) 201 | doc: """...""" 202 | 203 | flag_multi_arg host { ... } 204 | } 205 | } 206 | ``` 207 | 208 | This will add top-level flags: `--color, --no-color, -c, -C, --help, 209 | -h, --log-level, -l, --info, --warn, --error, --verbose`. 210 | 211 | It will also add a `run` command with its own sub-flags, generate a 212 | bunch of help docs, etc. 213 | 214 | And on the command line, by default, Con4m is as forgiving as 215 | possible. For example, it doesn't care about spaces around an '=', and 216 | if args are required, whether you drop it. Nor does it care if flags 217 | appear way before or after the command they're attached to (as long as 218 | there is no ambiguity). You can even have it try to guess the 219 | top-level command so that it can be omitted or provided as a default 220 | via config file. 221 | 222 | # Getting Started 223 | 224 | Currently, Con4m hasn't been officially released. We expect to provide 225 | a distro with the stand-alone compiler. But if you're interested, you 226 | could use it, or at least, follow along while we're working. 227 | 228 | Right now, you have to build from source, which requires Nim 229 | (https://nim-lang.org/), a systems language that's fast and has strong 230 | memory safety by default, yet somehow feels like Python. But it 231 | doesn't have much of an ecosystem. 232 | 233 | If you have Nim installed, you can easily install the current version 234 | with nimble: 235 | 236 | ```bash 237 | nimble install https://github.com/crashappsec/con4m 238 | ``` 239 | 240 | Then, you can run the `con4m` compiler, link to libcon4m, or, if 241 | you're a Nim user, simply `import con4m` 242 | 243 | # More Information 244 | 245 | There's a lot of documentation embedded in con4m, but we will focus on 246 | documentation later in the year. For now, the core capabilities are 247 | best documented in the Chalk documentation, since it's the Chalk 248 | config file format. 249 | 250 | [Chalk](https://github.com/crashappsec/chalk) 251 | 252 | # About 253 | 254 | Con4m is open source under the Apache 2.0 license. 255 | 256 | Con4m was written by John Viega (john@crashoverride.com), originally 257 | for Chalk and other to-be-named projects, because other options for 258 | flexibile configs (like HCL, or YAML DSLs) all kinda suck. 259 | -------------------------------------------------------------------------------- /con4m.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | version = "0.2.0" 3 | author = "John Viega" 4 | description = "A generic configuration file format that allows for flexible, lightweight scripting." 5 | license = "Apache-2.0" 6 | bin = @["con4m"] 7 | srcDir = "files" 8 | installExt = @["nim", "c4m", "c42spec", "sh"] 9 | 10 | # Dependencies 11 | requires "nim >= 2.0.0" 12 | requires "https://github.com/crashappsec/nimutils#a7ec24b27453342157cefe947e5730af5f5c05af" 13 | 14 | 15 | #before build: 16 | # let script = "files/bin/devmode.sh" 17 | # # only missing in Dockerfile compile step 18 | # if not fileExists(script): 19 | # return 20 | # exec script 21 | 22 | task ctest, "Build libcon4m": 23 | when hostOs == "linux" or hostOS == "macosx": 24 | exec "if [ ! -e lib ] ; then mkdir lib; fi" 25 | exec "if [ ! -e bin ] ; then mkdir bin; fi" 26 | exec "nim c --define:CAPI --app:staticlib --noMain:on src/con4m.nim" 27 | exec "mv src/libcon4m.a lib" 28 | exec "cc -Wall -o bin/test src/c/test.c lib/libcon4m.a -I ~/.choosenim/toolchains/nim-1.6.10/lib/ -lc -lm -ldl" 29 | else: 30 | echo "Platform ", hostOs, " Not supported." 31 | -------------------------------------------------------------------------------- /config.nims: -------------------------------------------------------------------------------- 1 | import strutils 2 | 3 | switch("define", "testCases") 4 | switch("debugger", "native") 5 | switch("d", "nimPreviewHashRef") 6 | switch("d", "ssl") 7 | switch("d", "useOpenSSL3") 8 | switch("gc", "refc") 9 | 10 | when (NimMajor, NimMinor) < (1, 7): 11 | # Locklevels never worked and are gone but warnings persist. 12 | switch("warning", "LockLevel:off") 13 | when (NimMajor, NimMinor, NimPatch) >= (1, 6, 12): 14 | # Someone made a move to deprecate, but they're undoing it. 15 | switch("warning", "BareExcept:off") 16 | 17 | when not defined(debug): 18 | switch("d", "release") 19 | switch("opt", "speed") 20 | 21 | var targetArch = hostCPU 22 | 23 | when defined(macosx): 24 | # -d:arch=amd64 will allow you to specifically cross-compile to intel. 25 | # The .strdefine. pragma sets the variable from the -d: flag w/ the same 26 | # name, overriding the value of the const. 27 | const arch {.strdefine.} = "detect" 28 | 29 | var 30 | targetStr = "" 31 | 32 | if arch == "detect": 33 | # On an x86 mac, the proc_translated OID doesn't exist. So if this 34 | # returns either 0 or 1, we know we're running on an arm. Right now, 35 | # nim will always use rosetta, so should always give us a '1', but 36 | # that might change in the future. 37 | let sysctlOut = staticExec("sysctl -n sysctl.proc_translated") 38 | 39 | if sysctlOut in ["0", "1"]: 40 | targetArch = "arm64" 41 | else: 42 | targetArch = "amd64" 43 | else: 44 | echo "Override: arch = " & arch 45 | 46 | if targetArch == "arm64": 47 | echo "Building for arm64" 48 | targetStr = "arm64-apple-macos13" 49 | elif targetArch == "amd64": 50 | targetStr = "x86_64-apple-macos13" 51 | echo "Building for amd64" 52 | else: 53 | echo "Invalid target architecture for MacOs: " & arch 54 | quit(1) 55 | 56 | switch("cpu", targetArch) 57 | switch("passc", "-flto -target " & targetStr) 58 | switch("passl", "-flto -w -target " & targetStr & 59 | "-Wl,-object_path_lto,lto.o") 60 | 61 | elif defined(linux): 62 | switch("passc", "-static") 63 | switch("passl", "-static") 64 | else: 65 | echo "Platform not supported." 66 | quit(1) 67 | 68 | var 69 | subdir = "" 70 | 71 | for item in listDirs(thisDir()): 72 | if item.endswith("/files"): 73 | subdir = "/files" 74 | break 75 | 76 | proc getEnvDir(s: string, default = ""): string = 77 | result = getEnv(s, default) 78 | if not result.endsWith("/"): 79 | result &= "/" 80 | 81 | exec thisDir() & subdir & "/bin/buildlibs.sh " & thisDir() & "/files/deps" 82 | 83 | var 84 | default = getEnvDir("HOME") & ".local/c0" 85 | localDir = getEnvDir("LOCAL_INSTALL_DIR", default) 86 | libDir = localdir & "libs" 87 | libs = ["pcre", "ssl", "crypto", "gumbo"] 88 | 89 | when defined(linux): 90 | var 91 | muslPath = localdir & "musl/bin/musl-gcc" 92 | switch("gcc.exe", muslPath) 93 | switch("gcc.linkerexe", muslPath) 94 | 95 | for item in libs: 96 | let libFile = "lib" & item & ".a" 97 | 98 | switch("passL", libDir & "/" & libFile) 99 | switch("dynlibOverride", item) 100 | -------------------------------------------------------------------------------- /files/bin/buildlibs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function color { 4 | case $1 in 5 | black) CODE=0 ;; 6 | red) CODE=1 ;; RED) CODE=9 ;; 7 | green) CODE=2 ;; GREEN) CODE=10 ;; 8 | yellow) CODE=3 ;; YELLOW) CODE=11 ;; 9 | blue) CODE=4 ;; BLUE) CODE=12 ;; 10 | magenta) CODE=5 ;; MAGENTA) CODE=13 ;; 11 | cyan) CODE=6 ;; CYAN) CODE=14 ;; 12 | white) CODE=7 ;; WHITE) CODE=15 ;; 13 | grey) CODE=8 ;; *) CODE=$1 ;; 14 | esac 15 | shift 16 | 17 | export TERM=${TERM:-vt100} 18 | echo -n $(tput -T ${TERM} setaf ${CODE})$@$(tput -T ${TERM} op) 19 | } 20 | 21 | function colorln { 22 | echo $(color $@) 23 | } 24 | 25 | if [[ ${#} -eq 0 ]] ; then 26 | colorln RED Script requires an argument pointing to the deps directory 27 | exit 1 28 | fi 29 | 30 | ARCH=$(uname -m) 31 | OS=$(uname -o 2>/dev/null) 32 | if [[ $? -ne 0 ]] ; then 33 | # Older macOS/OSX versions of uname don't support -o 34 | OS=$(uname -s) 35 | fi 36 | 37 | if [[ ${OS} = "Darwin" ]] ; then 38 | # Not awesome, but this is what nim calls it. 39 | OS=macosx 40 | 41 | # We might be running virtualized, so do some more definitive 42 | # tesitng. Note that there's no cross-compiling flag; if you 43 | # want to cross compile, you currently need to manually build 44 | # these libs. 45 | SYSCTL=$(sysctl -n sysctl.proc_translated 2>/dev/null) 46 | if [[ ${SYSCTL} = '0' ]] || [[ ${SYSCTL} == '1' ]] ; then 47 | NIMARCH=arm64 48 | else 49 | NIMARCH=amd64 50 | fi 51 | else 52 | # We don't support anything else at the moment. 53 | OS=linux 54 | if [[ ${ARCH} = "x86_64" ]] ; then 55 | NIMARCH=amd64 56 | else 57 | NIMARCH=arm64 58 | fi 59 | fi 60 | 61 | DEPS_DIR=${DEPS_DIR:-${HOME}/.local/c0} 62 | 63 | PKG_LIBS=${1}/lib/${OS}-${NIMARCH} 64 | MY_LIBS=${DEPS_DIR}/libs 65 | SRC_DIR=${DEPS_DIR}/src 66 | MUSL_DIR=${DEPS_DIR}/musl 67 | MUSL_GCC=${MUSL_DIR}/bin/musl-gcc 68 | 69 | mkdir -p ${MY_LIBS} 70 | 71 | # The paste doesn't work from stdin on MacOS, so leave this as is, please. 72 | export OPENSSL_CONFIG_OPTS=$(echo " 73 | enable-ec_nistp_64_gcc_128 74 | no-afalgeng 75 | no-apps 76 | no-bf 77 | no-camellia 78 | no-cast 79 | no-comp 80 | no-deprecated 81 | no-des 82 | no-docs 83 | no-dtls 84 | no-dtls1 85 | no-egd 86 | no-engine 87 | no-err 88 | no-idea 89 | no-md2 90 | no-md4 91 | no-mdc2 92 | no-psk 93 | no-quic 94 | no-rc2 95 | no-rc4 96 | no-rc5 97 | no-seed 98 | no-shared 99 | no-srp 100 | no-ssl 101 | no-tests 102 | no-tls1 103 | no-tls1_1 104 | no-uplink 105 | no-weak-ssl-ciphers 106 | no-zlib 107 | " | tr '\n' ' ') 108 | 109 | function copy_from_package { 110 | for item in ${@} 111 | do 112 | if [[ ! -f ${MY_LIBS}/${item} ]] ; then 113 | if [[ ! -f ${PKG_LIBS}/${item} ]] ; then 114 | return 1 115 | else 116 | cp ${PKG_LIBS}/${item} ${MY_LIBS} 117 | fi 118 | fi 119 | done 120 | return 0 121 | } 122 | 123 | function get_src { 124 | mkdir -p ${SRC_DIR} 125 | cd ${SRC_DIR} 126 | 127 | if [[ ! -d ${SRC_DIR}/${1} ]] ; then 128 | echo $(color CYAN Downloading ${1} from:) ${2} 129 | git clone ${2} 130 | fi 131 | if [[ ! -d ${1} ]] ; then 132 | echo $(color RED Could not create directory: ) ${SRC_DIR}/${1} 133 | exit 1 134 | fi 135 | cd ${1} 136 | } 137 | 138 | function ensure_musl { 139 | if [[ ${OS} = "macosx" ]] ; then 140 | return 141 | fi 142 | if [[ ! -f ${MUSL_GCC} ]] ; then 143 | # if musl-gcc is already installed, use it 144 | existing_musl=$(which musl-gcc 2> /dev/null) 145 | if [[ -n "${existing_musl}" ]]; then 146 | mkdir -p $(dirname ${MUSL_GCC}) 147 | ln -s ${existing_musl} ${MUSL_GCC} 148 | echo $(color GREEN Linking existing musl-gcc: ) ${existing_musl} $(color GREEN "->" ) ${MUSL_GCC} 149 | fi 150 | fi 151 | if [[ ! -f ${MUSL_GCC} ]] ; then 152 | get_src musl git://git.musl-libc.org/musl 153 | colorln CYAN Building musl 154 | unset CC 155 | ./configure --disable-shared --prefix=${MUSL_DIR} 156 | make clean 157 | make 158 | make install 159 | mv lib/*.a ${MY_LIBS} 160 | 161 | if [[ -f ${MUSL_GCC} ]] ; then 162 | echo $(color GREEN Installed musl wrapper to:) ${MUSL_GCC} 163 | else 164 | colorln RED Installation of musl failed! 165 | exit 1 166 | fi 167 | fi 168 | export CC=${MUSL_GCC} 169 | export CXX=${MUSL_GCC} 170 | } 171 | 172 | function install_kernel_headers { 173 | if [[ ${OS} = "macosx" ]] ; then 174 | return 175 | fi 176 | colorln CYAN Installing kernel headers needed for musl install 177 | get_src kernel-headers https://github.com/sabotage-linux/kernel-headers.git 178 | make ARCH=${ARCH} prefix= DESTDIR=${MUSL_DIR} install 179 | } 180 | 181 | function ensure_openssl { 182 | 183 | if ! copy_from_package libssl.a libcrypto.a ; then 184 | ensure_musl 185 | install_kernel_headers 186 | 187 | get_src openssl https://github.com/openssl/openssl.git 188 | colorln CYAN Building openssl 189 | if [[ ${OS} == "macosx" ]]; then 190 | ./config ${OPENSSL_CONFIG_OPTS} 191 | else 192 | ./config ${OPENSSL_CONFIG_OPTS} -static 193 | fi 194 | make clean 195 | make build_libs 196 | mv *.a ${MY_LIBS} 197 | if [[ -f ${MY_LIBS}/libssl.a ]] && [[ -f ${MY_LIBS}/libcrypto.a ]] ; then 198 | echo $(color GREEN Installed openssl libs to:) ${MY_LIBS} 199 | else 200 | colorln RED Installation of openssl failed! 201 | exit 1 202 | fi 203 | fi 204 | } 205 | 206 | function ensure_pcre { 207 | if ! copy_from_package libpcre.a ; then 208 | 209 | get_src pcre https://github.com/luvit/pcre.git 210 | colorln CYAN "Building libpcre" 211 | # For some reason, build fails on arm if we try to compile w/ musl? 212 | unset CC 213 | ./configure --disable-cpp --disable-shared 214 | make clean 215 | make 216 | 217 | mv .libs/libpcre.a ${MY_LIBS} 218 | if [[ -f ${MY_LIBS}/libpcre.a ]] ; then 219 | echo $(color GREEN Installed libpcre to:) ${MY_LIBS}/libpcre.a 220 | else 221 | colorln RED "Installation of libprce failed. This may be due to missing build dependencies. Please make sure autoconf, m4 and perl are installed." 222 | exit 1 223 | fi 224 | fi 225 | } 226 | 227 | function ensure_gumbo { 228 | if ! copy_from_package libgumbo.a ; then 229 | ensure_musl 230 | get_src sigil-gumbo https://github.com/Sigil-Ebook/sigil-gumbo/ 231 | colorln CYAN "Watching our waistline, selecting only required gumbo ingredients..." 232 | cat > CMakeLists.txt < /dev/null 28 | echo `pwd` 29 | popd > /dev/null 30 | } 31 | 32 | if [[ -z ${CON4M_DEV+woo} ]]; then exit; fi 33 | NIMUTILS_DIR=$(to_abs_dir ${NIMUTILS_DIR:-../nimutils}) 34 | NIMBLE_PKGS=$(to_abs_dir ${NIMBLE_PKGS:-~/.nimble/pkgs2}) 35 | 36 | echo $(color CYAN "nimutils direct: ") $NIMUTILS_DIR 37 | echo $(color CYAN "nimble packages: ") $NIMBLE_PKGS 38 | 39 | LATEST_NUTIL=`ls ${NIMBLE_PKGS} | egrep "^nimutils" | sort -V | tail -1` 40 | 41 | echo $(color CYAN "nimutils vers: ") $LATEST_NUTIL 42 | 43 | NIMUTILS_TARGET=${NIMBLE_PKGS}/${LATEST_NUTIL} 44 | 45 | function copy_news { 46 | # $1 -- source directory 47 | # $2 -- destination directory 48 | # $3 -- file name. 49 | 50 | SRC_FILE=$1/$3 51 | DST_FILE=$2/$3 52 | 53 | if [[ $SRC_FILE -nt $DST_FILE ]]; then 54 | if [[ ! -e $SRC_FILE ]]; then 55 | echo $(color RED error:) specified 'cp ${SRC_FILE} ${DST_FILE}' but the source file does not exist. 56 | else 57 | if [[ ! -e $DST_FILE ]]; then 58 | echo $(color YELLOW Copying new file: ) $3 59 | #echo $(color YELLOW to: ) $DST_FILE 60 | else 61 | echo $(color GREEN Updating file: ) $3 62 | #echo $(color GREEN full location: $DST_FILE) 63 | fi 64 | cp $SRC_FILE $DST_FILE 65 | fi 66 | fi 67 | } 68 | 69 | copy_news $NIMUTILS_DIR $NIMUTILS_TARGET nimutils.nimble 70 | copy_news $NIMUTILS_DIR $NIMUTILS_TARGET nimutils.nim 71 | 72 | function push_ext_files { 73 | # $1 is the src dir 74 | # $2 is the dst dir 75 | # $3 is the extension 76 | pushd $1 >/dev/null 77 | 78 | for item in `ls *.$3`; do 79 | copy_news $1 $2 $item 80 | done 81 | 82 | popd >/dev/null 83 | } 84 | 85 | push_ext_files ${NIMUTILS_DIR}/nimutils ${NIMUTILS_TARGET}/nimutils nim 86 | push_ext_files ${NIMUTILS_DIR}/nimutils/headers ${NIMUTILS_TARGET}/nimutils/headers nim 87 | -------------------------------------------------------------------------------- /files/c/test.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "con4m.h" 5 | 6 | char *configTest = "test section {\n attr = \"hello, world!\"\nf = 12\n}"; 7 | 8 | char * 9 | read_file(char *filename) 10 | { 11 | char *result = NULL; 12 | FILE *fp = fopen(filename, "rb"); 13 | long sz; 14 | 15 | if (fp == NULL) { 16 | return NULL; 17 | } 18 | if (!fseek(fp, 0, SEEK_END)) { 19 | sz = ftell(fp); 20 | 21 | if (fp < 0) { 22 | return NULL; 23 | } 24 | 25 | result = malloc(sz + 1); 26 | fseek(fp, 0, SEEK_SET); 27 | } else { 28 | return NULL; 29 | } 30 | if (fread(result, 1, sz, fp) < sz) { 31 | return NULL; 32 | } 33 | result[sz] = 0; 34 | 35 | return result; 36 | } 37 | 38 | int 39 | main(int argc, char *argv[], char *envp[]) 40 | { 41 | char *err; 42 | int64_t ignore; 43 | NimMain(); 44 | char *samispec = read_file("tests/spec/s2-sami.c4m"); 45 | C4Spec specobj = c4mLoadSpec(samispec, "tests/spec/s2-sami.c4m", &ignore); 46 | 47 | char *res = c4mOneShot(configTest, "whatevs.c4m"); 48 | printf("%s\n", res); 49 | c4mStrDelete(res); 50 | void *res2 = c4mFirstRun(configTest, "whatevs.c4m", 1, NULL, &err); 51 | if (!res2) { 52 | printf("%s", err); 53 | exit(0); 54 | } 55 | printf("res2 @%p\n", res2); 56 | assert(!c4mSetAttrInt(res2, "f", 14)); 57 | AttrErr errno = 0; 58 | printf("This should be 14: %ld\n", c4mGetAttrInt(res2, "f", &errno)); 59 | c4mSetAttrStr(res2, "foo", "bar"); 60 | printf("foo = %s\n", c4mGetAttrStr(res2, "foo", &errno)); 61 | 62 | char *chalkcfg = read_file("tests/samibase.c4m"); 63 | 64 | if (chalkcfg == NULL) { 65 | printf("Couldn't read test file.\n"); 66 | exit(0); 67 | } 68 | void *res3 = c4mFirstRun(chalkcfg, "samibase.c4m", 1, NULL, &err); 69 | if (!res3) { 70 | printf("%s", err); 71 | exit(0); 72 | } 73 | char **sects; 74 | int64_t num_sects, i; 75 | 76 | num_sects = c4GetSections(res3, "key", §s); 77 | 78 | for (i = 0; i < num_sects; i++) { 79 | printf("%s\n", sects[i]); 80 | } 81 | 82 | printf("\n---Fields for key 'METADATA_ID':\n"); 83 | 84 | char **fields; 85 | int64_t num_fields; 86 | num_fields = c4GetFields(res3, "key.METADATA_ID", &fields); 87 | for (i = 0; i < num_fields; i += 2) { 88 | printf("%s: %s\n", fields[i], fields[i + 1]); 89 | } 90 | 91 | c4mArrayDelete(sects); 92 | c4mArrayDelete(fields); 93 | printf("\nRoot scope contents:\n"); 94 | num_fields = c4EnumerateScope(res3, "", &fields); // Root scope. 95 | for (i = 0; i < num_fields; i += 2) { 96 | printf("%s: %s\n", fields[i], fields[i + 1]); 97 | } 98 | 99 | return 0; 100 | } 101 | -------------------------------------------------------------------------------- /files/con4m.nim: -------------------------------------------------------------------------------- 1 | ## Makes it easy to build Apache-style configuration files with 2 | ## well-defined schemas, where you don't have to do significant work. 3 | ## 4 | ## And the people who write configuration files, can do extensive 5 | ## customization using the con4m language, which is built in a way 6 | ## that guarantees termination (e.g., no while loops, for loop index 7 | ## variables are immutible to the programmer). 8 | ## 9 | ## :Author: John Viega (john@crashoverride.com) 10 | ## :Copyright: 2022 11 | 12 | import con4m/[errmsg, types, lex, parse, st, builtins, treecheck, typecheck, 13 | eval, dollars, spec, run, c42spec, getopts, stack, legacy, doc, 14 | components, strcursor, params] 15 | import streams, nimutils, os 16 | export errmsg, types, lex, parse, st, builtins, treecheck, typecheck, eval, 17 | dollars, spec, run, c42spec, getopts, stack, legacy, doc, components, 18 | strcursor, params 19 | 20 | const compilerC42FileName = "con4m/c4m/compiler-config.c42spec" 21 | const compilerConfigFName = "con4m/c4m/c4m-cmdline.c4m" 22 | const c4mc42Contents = staticRead(compilerC42FileName) 23 | const c4mconfigContents = staticRead(compilerConfigFName) 24 | 25 | when defined(CAPI): 26 | import con4m/capi 27 | export capi 28 | 29 | elif isMainModule: 30 | let 31 | specf = newStringStream(c4mc42Contents) 32 | cfgf = newStringStream(c4mconfigContents) 33 | conf = resolvePath("~/.config/con4m/con4m.conf") 34 | c4mstack = newConfigStack(). 35 | addSystemBuiltins(). 36 | addGetoptSpecLoad(). 37 | addSpecLoad(compilerC42FileName, specf). 38 | addConfLoad(compilerConfigFName, cfgf). 39 | addStartGetOpts(). 40 | addFinalizeGetOpts() 41 | 42 | discard subscribe(con4mTopic, defaultCon4mHook) 43 | 44 | if conf.fileExists(): 45 | try: 46 | let stream = newFileStream(conf) 47 | discard c4mstack.addConfLoad(conf, stream) 48 | except: 49 | stderr.write("Error: could not open external config file " & 50 | "(permissions issue?)") 51 | raise 52 | 53 | c4mstack.run(backtrace = true) 54 | 55 | let 56 | command = c4mstack.getCommand() 57 | config = c4mstack.getAttrs().get() 58 | args = c4mstack.getArgs() 59 | colorOpt = getOpt[bool](config, "color") 60 | specs = get[seq[string]](config, "specs") 61 | 62 | if colorOpt.isSome(): setShowColor(colorOpt.get()) 63 | 64 | setConfigState(config) 65 | if command == "run": 66 | con4mRun(args, specs) 67 | elif command == "gen": 68 | specgenRun(args) 69 | else: 70 | echo "Unknown command: ", command 71 | -------------------------------------------------------------------------------- /files/con4m/c4m/c4m-cmdline.c4m: -------------------------------------------------------------------------------- 1 | # Command line argument spec for the con4m command line. 2 | # TODO: add validators. 3 | 4 | con4m_command = "" # Will get filled in by getopts for us. 5 | con4m_args = [] 6 | 7 | getopts { 8 | default_yes_prefixes: [] 9 | default_no_prefixes: ["no"] 10 | show_doc_on_err: true 11 | add_help_commands: true 12 | command_attribute: "con4m_command" 13 | arg_attribute: "con4m_args" 14 | default_command: "run" 15 | doc: """ 16 | 17 | Con4m makes it EASY to build custom configuration files for your application that are fully validated to your specification. Your users can write in a simple, familiar format, but have access a lot more power, if needed. They can even easily write custom callbacks for you to call from your own environment. Plus, Con4m makes it easy to 'stack' configurations on top of each other, to customize environment variables, to reload configurations at runtime, etc. 18 | 19 | Con4m is accessible via command or API (currently from C or Nim, with Python and Go planned for 1.0). 20 | 21 | When running con4m the command line, by default, the 'run' command runs, which executes con4m configuration files, and outputs the resulting configuration state, either as JSON, or in a pretty-printed table. 22 | 23 | If multiple config files are provided, they are executed 'stacked', run one after another, in the order in which they appear on the command-line. 24 | 25 | Configurations can be validated extensively if you provide a 'c42' spec file to validate against. See 'help spec' for an example. Note that c42 specs are just config files that specify what is or isn't allowed in OTHER config files. 26 | 27 | urrently, con4m works natively with Nim, and provides a C API (libcon4m), which we intend to wrap for additional programming languages. 28 | 29 | The con4m compiler is capable of generating code to load native data structures and provide get/set interfaces for supported languages. To do this, you provide a schema in the form of a 'c42' spec file, and run the 'specgen' command. 30 | 31 | See: 32 | 'con4m.out help specs' for details on working with Con4m specifications for additional checking. 33 | 'con4m.out help con4m' for an overview of the con4m config file format (and language). 34 | 'con4m.out help topics' for a list of all available help topics. 35 | """ 36 | 37 | flag_yn color { 38 | yes_aliases: ["c"] 39 | no_aliases: ["C"] 40 | field_to_set: "color" 41 | doc: """ 42 | Enable ANSI colors in output, overriding the NO_COLORS environment variable 43 | """ 44 | } 45 | 46 | command run { 47 | aliases: ["c"] 48 | args: (1, high()) 49 | doc: """ 50 | """ 51 | 52 | flag_yn show_tokens { 53 | yes_aliases: ["k", "show-toks", "show-tok", "toks"] 54 | no_aliases: ["K", "no-show-toks", "no-show-tok", "no-toks"] 55 | field_to_set: "show_tokens" 56 | doc: """ 57 | Show tokens after the parsing phase, for the last specified con4m file passed. 58 | When multiple files are passed, only shows tokens for files based on the value of '--show-when' (which defaults to 'last'). 59 | """ 60 | } 61 | 62 | flag_yn show_parse_tree { 63 | yes_aliases: ["p", "show-parse", "parse"] 64 | no_aliases: ["P", "no-parse", "no-show-parse"] 65 | field_to_set: "show_parse_tree" 66 | doc: """ 67 | Shows the UNTYPED parse tree after the parsing phase, but before the checking phase. 68 | Applied based on '--show-when'. 69 | """ 70 | } 71 | 72 | flag_yn show_checked_tree { 73 | yes_aliases: ["t", "show-tree", "show-checked"] 74 | no_aliases: ["T", "no-show-tree", "no-show-checked"] 75 | field_to_set: "show_checked_tree" 76 | doc: """ 77 | It's Christmas! Show the typed parse tree after the checking phase. 78 | Applied based on '--show-when'. 79 | """ 80 | } 81 | 82 | flag_yn show_spec_tokens { 83 | yes_aliases: ["show-spec-toks", "show-spec-tok", "spec-toks"] 84 | no_aliases: ["no-show-spec-toks", "no-show-spec-tok", "no-spec-toks"] 85 | field_to_set: "show_spec_tokens" 86 | doc: """ 87 | Show tokens for c42 spec files after the parsing phase, for the last specified spec file passed. 88 | Applied based on the value of '--spec-show-when'. 89 | """ 90 | } 91 | 92 | flag_yn show_spec_parse_tree { 93 | yes_aliases: ["show-spec-parse", "spec-parse"] 94 | no_aliases: ["no-spec-parse", "no-spec-show-parse"] 95 | field_to_set: "show_spec_parse_tree" 96 | doc: """ 97 | Shows the unchecked parse tree for spec files. 98 | Applied based on '--spec-show-when'). 99 | """ 100 | } 101 | 102 | flag_yn show_spec_checked_tree { 103 | yes_aliases: ["spec-show-tree", "spec-show-checked"] 104 | no_aliases: ["no-spec-show-tree", "no-spec-show-checked"] 105 | field_to_set: "show_spec_checked_tree" 106 | doc: """ 107 | Specs deserve their time in lights. Show their typed parse tree after the checking phase. 108 | Applied based on '--spec-show-when'. 109 | """ 110 | } 111 | 112 | flag_yn show_funcs { 113 | yes_aliases: ["f", "funcs"] 114 | no_aliases: ["F", "no-funcs"] 115 | field_to_set: "show_funcs" 116 | doc: """ 117 | Show the function table, after checking but before evaluating. 118 | With multiple config files, this only ever gets applied in the last phase. 119 | 120 | If you want to see this before checking begins (when new functions will not be typed yet), use '--show-untyped-funcs'. 121 | 122 | This does not apply to spec files. 123 | """ 124 | } 125 | 126 | flag_yn show_untyped_funcs { 127 | yes_aliases: ["u", "untyped-funcs"] 128 | no_aliases: ["U", "no-untyped-funcs"] 129 | field_to_set: "show_untyped_funcs" 130 | doc: """ 131 | Shows the function table for the last file passed, before the checking phase begins, before functions are typed. Note that builtins and functions from previous files in the stack will be typed. 132 | """ 133 | } 134 | 135 | flag_choice output_style { 136 | aliases: ["output", "o"] 137 | choices: ["json", "pretty", "none"] 138 | field_to_set: "output_style" 139 | doc: """ 140 | If a file successfully executes, this determines the style for 141 | outputting the resulting attribute space. Options are: "json", 142 | "pretty" (which gives tables), or "none". The default is "json". 143 | 144 | When passing in a stack of files files, when this happens is controlled by '--output-show-when', which defaults to 'last'. 145 | 146 | Note that this output goes to stdout; all other output goes to stderr. 147 | """ 148 | } 149 | 150 | flag_multi_choice show_when { 151 | aliases: ["w", "when"] 152 | choices: ["first", "last", "rest", "all"] 153 | field_to_set: "show_when" 154 | min: 1 155 | max: high() 156 | doc: """ 157 | For --show-* flags and --no-show-* flags this indicates for which phases the flags will apply. Choices are "first", "last", "rest" and "all". The default will be 'last'. If there is only one file passed, this flag is ignored. 158 | 159 | Note that this does not apply to spec files; see 'spec_when'. 160 | 161 | This also doesn't control for which files we'll show the final attribute output. For that, see 'output_show_when'. 162 | """ 163 | } 164 | 165 | flag_multi_choice spec_when { 166 | aliases: ["spec-show-when"] 167 | choices: ["first", "last", "rest", "all"] 168 | field_to_set: "spec_when" 169 | min: 1 170 | max: high() 171 | doc: """ 172 | For --show- flags and --no-show- flags this indicates for which spec files passed at the command line where the flags will apply. Choices are "first", "last", "rest" and "all". The default will be 'last'. If there is only one spec file passed, this flag is ignored (same if there are zero). 173 | """ 174 | } 175 | 176 | flag_multi_choice output_when { 177 | aliases: ["output-show-when", "out-when", "out-show-when"] 178 | choices: ["first", "last", "rest", "all"] 179 | field_to_set: "output_when" 180 | min: 1 181 | max: high() 182 | doc: """ 183 | When multiple con4m files are executed in a stack, controls when the final attributes are shown at the end. By default, this is "last". 184 | 185 | Note, to turn off output, use --output-style=none, in which case this flag is ignored. 186 | """ 187 | } 188 | 189 | flag_multi_arg spec { 190 | aliases: ["specs"] 191 | field_to_set: "specs" 192 | doc: """ 193 | C42 spec file(s) to load (in the given order). They are loaded before any con4m file passed as arguments to 'run', and then are used to do additional validation those files. 194 | """ 195 | } 196 | 197 | flag_multi_arg stub { 198 | aliases: ["stubs"] 199 | field_to_set: "stubs" 200 | doc: """ 201 | Automatically provide stubs for any listed functions that do nothing, to allow the program to finish checking and execute. 202 | """ 203 | } 204 | 205 | flag_choice stop_when { 206 | aliases: ["stop", "x"] 207 | choices: ["tokenize", "parse", "precheck", "eval", "postcheck"] 208 | doc: """ 209 | Stop the parse AFTER the given phase. Valid values are 'tokenize', 'parse', 'precheck', 'eval' and 'postcheck'; 'postcheck' is the default. 210 | 211 | Note that this only is ever applied to the *last* file named on the command line. 212 | """ 213 | } 214 | } 215 | 216 | flag_yn debug { 217 | field_to_set: "debug_con4m" 218 | doc: """ 219 | Show *nim* stack traces when any error is output. This is intended to help diagnose compiler bugs. Note that we currently don't show con4m stack traces. 220 | """ 221 | } 222 | 223 | command gen { 224 | aliases: ["generate", "spec", "specgen"] 225 | args: (1, high()) 226 | doc: """ 227 | Auto-generates language-specific interface code, based on input spec files. 228 | """ 229 | flag_choice language { 230 | field_to_set: "language" 231 | choices: ["nim", "none"] 232 | doc: """ 233 | The language for which to generate output. The default is 'none', which simply validates the spec as a valid c42 specification file. Valid values are currently "none" (validates only), and "nim". We expect to do Python, C and Go before 1.0. 234 | """ 235 | } 236 | 237 | flag_arg output_file { 238 | field_to_set: "output_file" 239 | doc: """ 240 | Instead of writing to stdout, writes the generated code to the specified file. Ignored if the language is set to 'none'. 241 | """ 242 | } 243 | } 244 | 245 | flag_help { } 246 | } 247 | -------------------------------------------------------------------------------- /files/con4m/c4m/compiler-config.c42spec: -------------------------------------------------------------------------------- 1 | ## This is the con4m specification for the config file to set up the 2 | ## con4m command-line. 3 | ## 4 | ## None of this is generally used when linking to con4m, it is only 5 | ## used for the con4m command line. However, you can get this 6 | ## functionality programatically if you desire. 7 | 8 | root { 9 | user_def_ok: true 10 | 11 | field con4m_command { 12 | type: string 13 | default: "" 14 | hidden: true 15 | doc: """ 16 | This is set when parsing arguments, to tell us which command to run. 17 | """ 18 | } 19 | 20 | field con4m_args { 21 | type: list[string] 22 | default: [] 23 | hidden: true 24 | doc: """ 25 | This is set when parsing arguments. 26 | """ 27 | } 28 | 29 | field color { 30 | type: bool 31 | require: false 32 | shortdoc: "Show Colors" 33 | doc: """ 34 | Whether to output ANSI color. If this is not explicitly set, will respect 35 | the presense of a NO_ANSI environment variable, but is otherwise on by default. 36 | """ 37 | } 38 | 39 | field show_tokens { 40 | type: bool 41 | default: false 42 | shortdoc: "Show Tokens" 43 | doc: """ 44 | When compiling, show tokens after the parsing phase, for the last specified con4m file passed. 45 | When multiple files are passed, only shows tokens for files based on the value of 'show_when' (which defaults to 'last'). 46 | 47 | Override on the command line via '--show-tokens' or '--no-show-tokens' 48 | """ 49 | } 50 | 51 | field show_parse_tree { 52 | type: bool 53 | default: false 54 | shortdoc: "Show Parse Results" 55 | doc: """ 56 | Shows the UNTYPED parse tree after the parsing phase, but before the checking phase. 57 | 58 | Applied based on the value of 'show_when'. 59 | 60 | Override on the command line via '--show-parse-tree' (aka '-p' or '--parse'), or '--no-show-parse-tree' (aka '-P' or '--no-parse') 61 | """ 62 | } 63 | 64 | field show_checked_tree { 65 | type: bool 66 | default: false 67 | shortdoc: "Show Typed Tree" 68 | doc: """ 69 | It's Christmas! Show the typed parse tree after the checking phase. 70 | Applied based on 'show_when'. 71 | 72 | Override on the command line via '--show-checked-tree' (aka '-t' or '--show-tree'), or '--no-show-checked-tree (aka '-T' or '--no-show-tree') 73 | """ 74 | } 75 | 76 | field show_spec_tokens { 77 | type: bool 78 | default: false 79 | shortdoc: "Show Tokens for Specs" 80 | doc: """ 81 | When compiling, show tokens for c42 spec files after the parsing phase, for the last specified spec file passed. 82 | 83 | Applied based on the value of 'spec_show_when'. 84 | 85 | Override on the command line via '--show-spec-tokens' or '--no-show-spec-tokens' 86 | """ 87 | } 88 | 89 | field show_spec_parse_tree { 90 | type: bool 91 | default: false 92 | shortdoc: "Show C42 Spec Parse" 93 | doc: """ 94 | Shows the uncheck parse tree for spec files. 95 | Applied based on the value of 'show_spec_when'. 96 | 97 | Override on the command line via '--show-spec-parse-tree' (aka '--spec-parse'), or '--no-show-spec-parse-tree' (aka '--no-spec-parse') 98 | """ 99 | } 100 | 101 | field show_spec_checked_tree { 102 | type: bool 103 | default: false 104 | shortdoc: "Show C42 Typed Tree" 105 | doc: """ 106 | Specs deserve their time in lights. Show their typed parse tree after the checking phase. 107 | Applied based on the value of 'show_spec_when'. 108 | 109 | Override on the command line via '--show-spec-checked-tree' (aka '--spec-show-tree'), or '--no-show-spec-checked-tree (aka '--no-spec-show-tree') 110 | """ 111 | } 112 | 113 | field specs { 114 | type: list[string] 115 | default: [] 116 | doc: """ 117 | C42 spec file(s) to load (in the given order). They are loaded before any con4m file passed as arguments to 'run', and then are used to do additional validation those files. 118 | 119 | Override on the command line via --specs=... 120 | 121 | This is ignored via API calls; you're expected to explicitly add your own specs. 122 | """ 123 | } 124 | 125 | field show_funcs { 126 | type: bool 127 | default: false 128 | doc: """ 129 | Show the function table, after checking but before evaluating. 130 | With multiple config files, this only ever gets applied in the last phase. 131 | 132 | If you want to see this before checking begins (when new functions will not be typed yet), use 'show_untyped_funcs'. 133 | 134 | Override on the command line via '--show-funcs' (aka '-f', '--funcs') or '--no-show-funcs' (aka '-F', '--no-funcs') 135 | 136 | This attribute does not apply to spec files. 137 | """ 138 | } 139 | 140 | field show_untyped_funcs { 141 | type: bool 142 | default: false 143 | doc: """ 144 | Shows the function table for the last file passed, before the checking phase begins, before functions are typed. Note that builtins and functions from previous files in the stack will be typed. 145 | 146 | Override on the command line via '--show-untyped-funcs' (aka '-u', '--untyped-funcs') or '--no-show-untyped-funcs' (aka '-U', '--no-untyped-funcs') 147 | 148 | This attribute does not apply to spec files. 149 | """ 150 | } 151 | 152 | field output_style { 153 | type: string 154 | choice: ["json", "pretty", "raw", "none"] 155 | default: "json" 156 | 157 | doc: """ 158 | If a file successfully executes, this determines the style for 159 | outputting the resulting attribute space. Options are: "json", 160 | "pretty" (which gives tables), "raw" (which gives unfomatted json) 161 | or "none". The default is "json". 162 | 163 | When passing in a stack of files files, when this happens is controlled by 'output_show_when', which defaults to 'last'. 164 | 165 | Note that this output goes to stdout; all other output goes to stderr. 166 | 167 | On the command line, this can be overridden with '--output-style=...' (aka '-o') 168 | 169 | This field is never used when programatically invoking a con4m stack; you're expected to print the results you need yourself (the debug outputting of intermediate states *is* however observed). 170 | """ 171 | } 172 | 173 | field show_when { 174 | type: list[string] 175 | validator: func show_when_check 176 | default: ["last"] 177 | doc: """ 178 | For show_* fields, this indicates for which phases of a stack the option will apply. Choices are "first", "last", "rest", "none" or "all". The default will be 'last'. The 'first' item is the first item in any stack you create, and the 'last' item is the last most item every time you call run() on a stack. If you are incrementally running a stack from your own program, you should set this to "none" until you are ready to have the flags apply, then set this before running your stack. 179 | 180 | Note that this does not apply to spec files; that's done with the field 'spec_when'. 181 | 182 | This also doesn't control for which files we'll show the final attribute output. For that, see 'output_when'. 183 | 184 | On the command line, this can be overridden with '--show-when=...' (aka '-w') 185 | """ 186 | } 187 | 188 | field spec_when { 189 | type: list[string] 190 | validator: func show_when_check 191 | default: ["last"] 192 | doc: """ 193 | Same as show_when, but applied to spec files in a stack. 194 | On the command line, this can be overridden with '--spec-when=...' 195 | """ 196 | } 197 | 198 | field output_when { 199 | type: list[string] 200 | validator: func show_when_check 201 | default: ["last"] 202 | doc: """ 203 | This is only checked when using con4m on the command-line, not via API. It controls whether attributes are printed after a config gile completes execution or not. Valid choices are "first", "last", rest", or "all". 204 | 205 | Shutting off all output is handled by setting the output style, which is the attribute 'output_style'. 206 | 207 | This value can be overriden on the command line with '--output-when='. 208 | """ 209 | } 210 | 211 | field stubs { 212 | type: list[string] 213 | default: [] 214 | doc: """ 215 | Automatically generates functions for the named items with the con4m signatures you provide. Those functions will do nothing. However, this is an easy way to allow full checking of files via the con4m command line, even if you provide application-specific builtins. Basically, just drop the signatures of those builtins into this field in ~/.config/con4m/con4m.conf and go! 216 | 217 | This currently only works from the command line. 218 | """ 219 | } 220 | 221 | field stop_when { 222 | type: string 223 | choice: ["tokenize", "parse", "precheck", "eval", "postcheck"] 224 | default: "postcheck" 225 | doc: """ 226 | For the last file in a stack of con4m files, stop evaluating after the specified phase. The default is 'postcheck'. 227 | 228 | You can override this on the command line via '--stop-when' (aka '--stop', '-x') 229 | """ 230 | } 231 | 232 | field debug_con4m { 233 | type: bool 234 | default: false 235 | doc: """ 236 | When true, this will cause con4m to produce *nim* stack traces any time an error is output. This is intended to help diagnose compiler bugs. There currently is no ability to see con4m stack traces. 237 | """ 238 | } 239 | 240 | field language { 241 | type: string 242 | choice: ["nim", "none"] 243 | default: "none" 244 | doc: """ 245 | When running the con4m 'gen' command via the command line, this field determines the language for which to generate output. The default is 'none', which simply validates the spec as a valid c42 specification file. Valid values are currently "none" (validates only), and "nim". We expect to do Python, C and Go before 1.0. 246 | 247 | This field does nothing when using con4m programatically. 248 | 249 | On the command line, you can override this with the '--language=' flag. 250 | """ 251 | } 252 | 253 | field output_file { 254 | type: string 255 | require: false 256 | doc: """ 257 | When running the con4m 'gen' command from the command line, this field sets the location for writing any generated output. If not provided, any output will go to stdout. 258 | 259 | This is not ever used if calling con4m via API. 260 | 261 | On the command line, override this with '--output-file=' 262 | """ 263 | } 264 | } 265 | 266 | when_opts := ["first", "last", "rest", "all"] 267 | export when_opts 268 | 269 | func show_when_check(path, val: list[string]) { 270 | result := "" 271 | 272 | if val.contains("all") and len(val) != 1 { 273 | return "'all' may not appear with other options." 274 | } 275 | for i from 0 to len(val) { 276 | if not (when_opts.contains(val[i])) { 277 | return "Unknown value; valid options are: first, last, rest, all" 278 | } 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /files/con4m/capi.nim: -------------------------------------------------------------------------------- 1 | import types, streams, legacy, st, tables, nimutils, c42spec, strutils, dollars 2 | 3 | type C4CSpecObj = ref object 4 | spec: ConfigSpec 5 | state: ConfigState 6 | err: string 7 | 8 | # I need to spend more time investigating under the hood. I should 9 | # just be able to GC_ref() the string a cstring was based on, and have 10 | # it stick around. But I think there's perhaps some weird copying 11 | # happening, related to string sharing. So until I have time to 12 | # investigate, I'm just keeping a table mapping the memory address to 13 | # the object. Basically, the same underlying string might end up w/ 2 14 | # addresses pointing to it, and we want to keep each one live even if 15 | # we deref the other. Even w/ a C string, Nim's hash function uses 16 | # the contents of the string, not the pointer. 17 | var strPool = initTable[uint, cstring]() 18 | 19 | proc exportStr(s: string): cstring {.inline.} = 20 | result = cstring(s) 21 | strPool[cast[uint](result)] = result 22 | 23 | proc c4mStrDelete*(s: cstring) {.exportc, dynlib.} = 24 | strPool.del(cast[uint](s)) 25 | 26 | proc c4mOneShot*(contents: cstring, fname: cstring): cstring {.exportc, dynlib.} = 27 | try: 28 | let (ctx, res) = firstRun($(contents), $(fname)) 29 | if not res: 30 | return nil 31 | return exportStr(ctx.attrs.scopeToJson()) 32 | except: 33 | return exportStr(getCurrentExceptionMsg()) 34 | 35 | proc c4mFirstRun*(contents: cstring, fname: cstring, addBuiltIns: bool, 36 | spec: C4CSpecObj, err: var cstring): ConfigState {.exportc, dynlib.} = 37 | var 38 | c42Spec: ConfigSpec = nil 39 | specCtx: ConfigState = nil 40 | 41 | if spec != nil: 42 | c42Spec = spec.spec 43 | specCtx = spec.state 44 | 45 | try: 46 | let (ctx, res) = firstRun($(contents), 47 | $(fname), 48 | c42Spec, 49 | addBuiltIns, 50 | evalCtx = specCtx) 51 | if not res: 52 | var cstr = exportStr(getCurrentExceptionMsg()) 53 | err = cstr 54 | return nil 55 | GC_ref(ctx) 56 | return ctx 57 | except: 58 | var cstr = exportStr("Unknown error") 59 | err = cstr 60 | return nil 61 | 62 | proc c4mStack*(state: ConfigState, contents: cstring, fname: cstring, 63 | spec: C4CSpecObj): cstring {.exportc, dynlib.} = 64 | var specCtx: ConfigState = nil 65 | 66 | if spec != nil: 67 | specCtx = spec.state 68 | 69 | try: 70 | discard stackConfig(state, $(contents), $(fname), specCtx) 71 | return nil 72 | except: 73 | return exportStr(getCurrentExceptionMsg()) 74 | 75 | 76 | proc c4mSetAttrInt*(state: ConfigState, name: cstring, val: int): 77 | int {.exportc, dynlib.} = 78 | var 79 | n = $(name) 80 | b = pack[int](val) 81 | r = attrSet(state, n, b) 82 | return int(r.code) 83 | 84 | proc c4mGetAttrInt*(state: ConfigState, name: cstring, ok: ptr int): 85 | int {.exportc, dynlib.} = 86 | var 87 | n = $(name) 88 | (err, o) = attrLookupFull(state, n) 89 | 90 | ok[] = int(err) 91 | if err != errOk: 92 | return 0 93 | if o.isNone(): 94 | ok[] = int(errNoAttr) 95 | return 0 96 | 97 | let box = o.get() 98 | result = unpack[int](box) 99 | 100 | proc c4mSetAttrBool*(state: ConfigState, name: cstring, val: int): 101 | int {.exportc, dynlib.} = 102 | var 103 | n = $(name) 104 | b = pack[bool](if val != 0: true else: false) 105 | r = attrSet(state, n, b) 106 | return int(r.code) 107 | 108 | proc c4mGetAttrBool*(state: ConfigState, name: cstring, ok: ptr int): 109 | int {.exportc, dynlib.} = 110 | var 111 | n = $(name) 112 | (err, o) = attrLookupFull(state, n) 113 | 114 | ok[] = int(err) 115 | if err != errOk: 116 | return 0 117 | if o.isNone(): 118 | ok[] = int(errNoAttr) 119 | return 0 120 | 121 | let box = o.get() 122 | result = if unpack[bool](box): 1 else: 0 123 | 124 | 125 | proc c4mSetAttrStr*(state: ConfigState, name: cstring, val: cstring): 126 | int {.exportc, dynlib.} = 127 | var 128 | n = $(name) 129 | v = $(val) 130 | b = pack[string](v) 131 | r = attrSet(state, n, b) 132 | 133 | result = int(r.code) 134 | 135 | proc c4mGetAttrStr*(state: ConfigState, name: cstring, ok: ptr int): 136 | cstring {.exportc, dynlib.} = 137 | var 138 | n = $(name) 139 | (err, o) = attrLookupFull(state, n) 140 | 141 | ok[] = int(err) 142 | if err != errOk: 143 | return nil 144 | if o.isNone(): 145 | ok[] = int(errNoAttr) 146 | return nil 147 | let 148 | box = o.get() 149 | res = unpack[string](box) 150 | 151 | result = exportStr(res) 152 | 153 | proc c4mSetAttrFloat*(state: ConfigState, name: cstring, val: float): 154 | int {.exportc, dynlib.} = 155 | var 156 | n = $(name) 157 | b = pack[float](val) 158 | r = attrSet(state, n, b) 159 | 160 | result = int(r.code) 161 | 162 | proc c4mGetAttrFloat*(state: ConfigState, name: cstring, ok: ptr int): 163 | float {.exportc, dynlib.} = 164 | var 165 | n = $(name) 166 | (err, o) = attrLookupFull(state, n) 167 | 168 | ok[] = int(err) 169 | if err != errOk: 170 | return 0 171 | if o.isNone(): 172 | ok[] = int(errNoAttr) 173 | return NaN 174 | 175 | let 176 | box = o.get() 177 | result = unpack[float](box) 178 | 179 | proc c4mSetAttr*(state: ConfigState, name: cstring, b: Box): int {.exportc, dynlib.} = 180 | var 181 | n = $(name) 182 | r = attrSet(state, n, b) 183 | 184 | result = int(r.code) 185 | 186 | proc c4mGetAttr*(state: ConfigState, 187 | name: cstring, 188 | boxType: ptr MixedKind, 189 | ok: ptr int): Box {.exportc, dynlib.} = 190 | var 191 | n = $(name) 192 | (err, o) = attrLookupFull(state, n) 193 | 194 | ok[] = int(err) 195 | if err != errOk: 196 | return nil 197 | if o.isNone(): 198 | ok[] = int(errNoAttr) 199 | return nil 200 | 201 | result = o.get() 202 | boxType[] = result.kind 203 | 204 | GC_ref(result) 205 | 206 | proc c4mGetSections*(state: ConfigState, 207 | name: cstring, 208 | arr: var ptr cstring): int {.exportc, dynlib.} = 209 | var res: seq[cstring] = @[] 210 | 211 | let 212 | parts = `$`(name).split(".") 213 | aOrE = attrLookup(state.attrs, parts, 0, vlSecUse); 214 | 215 | if aOrE.isA(AttrErr): 216 | arr = nil 217 | return -1 218 | 219 | let aOrS = aOrE.get(AttrOrSub) 220 | 221 | if aOrS.isA(Attribute): 222 | arr = nil 223 | return -2 224 | 225 | let scope = aOrS.scope 226 | 227 | for k, v in scope.contents: 228 | if v.kind == false: 229 | res.add(cstring(k)) 230 | 231 | if len(res) == 0: 232 | arr = nil 233 | else: 234 | arr = addr(res[0]) 235 | GC_ref(res) 236 | 237 | return len(res) 238 | 239 | 240 | proc c4mGetFields*(state: ConfigState, 241 | name: cstring, 242 | arr: var ptr cstring): int {.exportc, dynlib.} = 243 | var res: seq[cstring] = @[] 244 | 245 | let 246 | parts = `$`(name).split(".") 247 | aOrE = attrLookup(state.attrs, parts, 0, vlSecUse); 248 | 249 | if aOrE.isA(AttrErr): 250 | arr = nil 251 | return -1 252 | 253 | let aOrS = aOrE.get(AttrOrSub) 254 | 255 | if aOrS.isA(Attribute): 256 | arr = nil 257 | return -2 258 | 259 | let scope = aOrS.scope 260 | 261 | for k, v in scope.contents: 262 | if v.isA(Attribute): 263 | res.add(cstring(k)) 264 | res.add(cstring($(v.get(Attribute).tInfo))) 265 | 266 | if len(res) == 0: 267 | arr = nil 268 | else: 269 | arr = addr(res[0]) 270 | GC_ref(res) 271 | 272 | return len(res) 273 | 274 | 275 | proc c4mEnumerateScope*(state: ConfigState, 276 | name: cstring, 277 | arr: var ptr cstring): int {.exportc, dynlib.} = 278 | 279 | var res: seq[cstring] = @[] 280 | 281 | let 282 | parts = `$`(name).split(".") 283 | aOrE = attrLookup(state.attrs, parts, 0, vlSecUse); 284 | 285 | if aOrE.isA(AttrErr): 286 | arr = nil 287 | return -1 288 | 289 | let aOrS = aOrE.get(AttrOrSub) 290 | 291 | if aOrS.isA(Attribute): 292 | arr = nil 293 | return -2 294 | 295 | let scope = aOrS.scope 296 | 297 | for k, v in scope.contents: 298 | res.add(cstring(k)) 299 | if v.isA(Attribute): 300 | res.add(cstring($(v.get(Attribute).tInfo))) 301 | else: 302 | res.add(cstring("section")) 303 | 304 | result = len(res) 305 | if result == 0: 306 | arr = nil 307 | else: 308 | arr = addr(res[0]) 309 | GC_ref(res) 310 | 311 | return len(res) 312 | 313 | proc c4mBoxType*(box: Box): MixedKind {.exportc, dynlib.} = 314 | return box.kind 315 | 316 | proc c4mClose*(state: ConfigState) {.exportc, dynlib.} = 317 | GC_unref(state) 318 | 319 | proc c4mUnpackInt*(box: Box): int {.exportc, dynlib.} = 320 | result = unpack[int](box) 321 | 322 | proc c4mPackInt*(i: int): Box {.exportc, dynlib.} = 323 | result = pack(i) 324 | GC_ref(result) 325 | 326 | proc c4mUnpackBool*(box: Box): int {.exportc, dynlib.} = 327 | result = if unpack[bool](box): 1 else: 0 328 | 329 | proc c4mPackBool*(i: int): Box {.exportc, dynlib.} = 330 | result = if i == 0: pack(false) else: pack(true) 331 | GC_ref(result) 332 | 333 | proc c4mUnpackFloat*(box: Box): float {.exportc, dynlib.} = 334 | result = unpack[float](box) 335 | 336 | proc c4mPackFloat*(f: float): Box {.exportc, dynlib.} = 337 | result = pack(f) 338 | GC_ref(result) 339 | 340 | proc c4mUnpackString*(box: Box): cstring {.exportc, dynlib.} = 341 | result = exportStr(unpack[string](box)) 342 | 343 | proc c4mPackString*(s: cstring): Box {.exportc, dynlib.} = 344 | result = pack($(s)) 345 | GC_ref(result) 346 | 347 | proc c4mUnpackArray*(box: Box, arr: var ptr Box): int {.exportc, dynlib.} = 348 | var items = unpack[seq[Box]](box) 349 | result = len(items) 350 | 351 | if result == 0: 352 | arr = nil 353 | else: 354 | arr = addr(items[0]) 355 | GC_ref(items) 356 | 357 | proc c4mArrayDelete*(arr: var seq[Box]) {.exportc, dynlib.} = 358 | GC_unref(arr) 359 | 360 | proc c4mPackArray*(arr: UncheckedArray[Box], sz: int): Box {.exportc, dynlib.} = 361 | var s: seq[Box] = @[] 362 | for i in 0 ..< sz: 363 | s.add(arr[i]) 364 | result = pack(s) 365 | GC_ref(result) 366 | 367 | proc c4mUnpackDict*(box: Box): OrderedTableRef[Box, Box] {.exportc, dynlib.} = 368 | result = unpack[OrderedTableRef[Box, Box]](box) 369 | GC_ref(result) 370 | 371 | proc c4mDictNew*(): OrderedTableRef[Box, Box] {.exportc, dynlib.} = 372 | result = newOrderedTable[Box, Box]() 373 | GC_ref(result) 374 | 375 | proc c4mDictDelete*(dict: OrderedTableRef[Box, Box]) {.exportc, dynlib.} = 376 | GC_unref(dict) 377 | 378 | proc c4mDictLookup*(tbl: OrderedTableRef[Box, Box], b: Box): Box {.exportc, dynlib.} = 379 | if b in tbl: 380 | result = tbl[b] 381 | GC_ref(result) 382 | else: 383 | return nil 384 | 385 | proc c4mDictSet*(tbl: OrderedTableRef[Box, Box], b: Box, v: Box) {.exportc, dynlib.} = 386 | tbl[b] = v 387 | 388 | proc c4mDictKeyDel*(tbl: OrderedTableRef[Box, Box], b: Box) {.exportc, dynlib.} = 389 | if b in tbl: 390 | tbl.del(b) 391 | 392 | proc c4mLoadSpec*(spec, fname: cstring, ok: ptr int): C4CSpecObj {.exportc, dynlib.} = 393 | result = new(C4CSpecObj) 394 | 395 | try: 396 | let opt = c42Spec(newStringStream($(spec)), $(fname)) 397 | 398 | if opt.isNone(): 399 | ok[] = int(0) 400 | result.spec = nil 401 | result.state = nil 402 | result.err = "" 403 | 404 | let (spec, evalCtx) = opt.get() 405 | 406 | result.spec = spec 407 | result.state = evalCtx 408 | result.err = "" 409 | ok[] = int(1) 410 | except: 411 | result.spec = nil 412 | result.state = nil 413 | result.err = getCurrentExceptionMsg() 414 | ok[] = int(0) 415 | 416 | GC_ref(result) 417 | 418 | 419 | proc c4mGetSpecErr*(spec: var C4CSpecObj): cstring {.exportc, dynlib.} = 420 | result = exportStr(spec.err) 421 | GC_unref(spec) 422 | 423 | proc c4mSpecDelete*(spec: var C4CSpecObj) {.exportc, dynlib.} = 424 | GC_unref(spec) 425 | 426 | proc c4mStateDelete*(state: var ConfigState) {.exportc, dynlib.} = 427 | GC_unref(state) 428 | 429 | proc c4mBoxDelete*(box: var Box) {.exportc, dynlib.} = 430 | GC_unref(box) 431 | -------------------------------------------------------------------------------- /files/con4m/components.nim: -------------------------------------------------------------------------------- 1 | import os, tables, streams, strutils, httpclient, net, uri, options, nimutils, 2 | types, lex, typecheck, errmsg 3 | 4 | # This has some cyclic dependencies, so we make sure C prototypes get 5 | # generated with local scope only; we then do not import those modules. 6 | 7 | proc parse(s: seq[Con4mToken], filename: string): Con4mNode {.importc, cdecl.} 8 | proc checkTree(node: Con4mNode, s: ConfigState) {.importc, cdecl.} 9 | 10 | var defaultUrlStore = "" 11 | 12 | proc fullComponentSpec*(name, location: string): string = 13 | var path: string 14 | if location == "": 15 | if defaultUrlStore != "": 16 | path = defaultUrlStore 17 | else: 18 | path = getAppDir().resolvePath() 19 | 20 | elif location.startsWith("https://"): 21 | path = location 22 | 23 | else: 24 | path = location.resolvePath() 25 | 26 | if path.startsWith("https://"): 27 | result = path & "/" & name 28 | else: 29 | result = path.joinPath(name) 30 | 31 | proc setDefaultStoreUrl*(url: string) = 32 | once: 33 | defaultUrlStore = url 34 | 35 | proc getComponentReference*(s: ConfigState, url: string): ComponentInfo = 36 | if url notin s.components: 37 | s.components[url] = ComponentInfo(url: url) 38 | 39 | return s.components[url] 40 | 41 | proc getComponentReference*(s: ConfigState, name, loc: string): ComponentInfo = 42 | return s.getComponentReference(fullComponentSpec(name, loc)) 43 | 44 | proc fetchAttempt(url: string): string = 45 | var 46 | uri = parseUri(url) 47 | context = newContext(verifyMode = CVerifyPeer) 48 | client = newHttpClient(sslContext = context, timeout = 1000) 49 | response = client.safeRequest(url = uri, httpMethod = HttpGet) 50 | 51 | if response.status[0] != '2': 52 | return "" 53 | 54 | return response.bodyStream.readAll() 55 | 56 | proc cacheComponent*(component: ComponentInfo, str: string, force = false) = 57 | if component.entrypoint != nil and not force: 58 | return 59 | 60 | component.source = str 61 | component.hash = sha256(component.source) 62 | 63 | let (valid, toks) = component.source.lex(component.url) 64 | 65 | if not valid: 66 | let msg = case toks[^1].kind 67 | of ErrorTok: "Invalid character found" 68 | of ErrorLongComment: "Unterminated comment" 69 | of ErrorStringLit: "Unterminated string" 70 | of ErrorCharLit: "Invalid char literal" 71 | of ErrorOtherLit: "Unterminated literal" 72 | else: "Unknown error" # Not be possible w/o a lex bug 73 | fatal(msg, toks[^1]) 74 | 75 | component.entrypoint = toks.parse(component.url) 76 | 77 | proc cacheComponent*(component: ComponentInfo, stream: Stream) = 78 | component.cacheComponent(stream.readAll()) 79 | 80 | proc fetchComponent*(item: ComponentInfo, extension = ".c4m", force = false) = 81 | let fullPath = item.url & extension 82 | var source: string 83 | 84 | if force or item.hash == "": 85 | if fullPath.startsWith("https://"): 86 | source = fullPath.fetchAttempt() 87 | 88 | if source == "": 89 | raise newException(IOError, "Could not retrieve needed source " & 90 | "file: " & fullPath) 91 | elif fullPath.startsWith("http:"): 92 | raise newException(IOError, "Insecure (http) loads are not allowed" & 93 | "(file: " & fullPath & ")") 94 | else: 95 | try: 96 | source = fullPath.readFile() 97 | except: 98 | raise newException(IOError, "Could not retrieve needed source " & 99 | "file: " & fullPath) 100 | 101 | item.cacheComponent(source, force) 102 | 103 | proc fetchComponent*(s: ConfigState, name, loc: string, extension = ".c4m", 104 | force = false): ComponentInfo = 105 | ## This returns a parsed component, but does NOT go beyond that. The 106 | ## parse phase will NOT attempt to semantically validate a component, 107 | ## will NOT go and fetch dependent comonents, and will NOT do cycle 108 | ## checking at all. Use loadComponent below for those. 109 | ## 110 | ## This will raise an exception if anything goes wrong. 111 | 112 | result = s.getComponentReference(name, loc) 113 | 114 | result.fetchComponent(extension, force) 115 | 116 | proc getUsedComponents*(component: ComponentInfo, paramOnly = false): 117 | seq[ComponentInfo] = 118 | var 119 | allDependents: seq[ComponentInfo] = @[component] 120 | 121 | for sub in component.componentsUsed: 122 | if sub notin result: 123 | result.add(sub) 124 | let sublist = sub.getUsedComponents() 125 | for item in sublist: 126 | if item == component: 127 | raise newException(ValueError, "Cyclical components not allowed-- " & 128 | "component " & component.url & " can import itself") 129 | if item notin allDependents: 130 | allDependents.add(item) 131 | 132 | if not paramOnly: 133 | return allDependents 134 | else: 135 | for item in allDependents: 136 | if item.varParams.len() != 0 or item.attrParams.len() != 0: 137 | if item notin result: 138 | result.add(item) 139 | 140 | proc loadComponent*(s: ConfigState, component: ComponentInfo): 141 | seq[ComponentInfo] {.discardable.} = 142 | ## Recursively fetches any dependent components (if not cached) and 143 | ## checks them. 144 | 145 | if component.cycle: 146 | raise newException(ValueError, "Cyclical components are not allowed-- " & 147 | "component " & component.url & " can import itself") 148 | 149 | if component.hash == "": 150 | component.fetchComponent() 151 | 152 | let 153 | savedComponent = s.currentComponent 154 | savedPass = s.secondPass 155 | 156 | 157 | if not component.typed: 158 | s.secondPass = false 159 | s.currentComponent = component 160 | component.entryPoint.varScope = VarScope(parent: none(VarScope)) 161 | 162 | component.entrypoint.checkTree(s) 163 | component.typed = true 164 | 165 | for subcomponent in component.componentsUsed: 166 | s.loadComponent(subcomponent) 167 | 168 | s.currentComponent = savedComponent 169 | s.secondPass = savedPass 170 | 171 | for subcomponent in component.componentsUsed: 172 | component.cycle = true 173 | let recursiveUsedComponents = s.loadComponent(subcomponent) 174 | component.cycle = false 175 | for item in recursiveUsedComponents: 176 | if item notin result: 177 | result.add(item) 178 | 179 | if component in result: 180 | raise newException(ValueError, "Cyclical components are not allowed-- " & 181 | "component " & component.url & " can import itself") 182 | 183 | proc fullUrlToParts*(url: string): (string, string, string) = 184 | var fullPath: string 185 | 186 | if url.startswith("http://"): 187 | raise newException(ValueError, "Only https URLs and local files accepted") 188 | if url.startswith("https://") or url.startswith("/"): 189 | fullPath = url 190 | else: 191 | if '/' in url or defaultUrlStore == "": 192 | fullPath = url.resolvePath() 193 | else: 194 | if defaultUrlStore.endswith("/"): 195 | fullPath = defaultUrlStore & url 196 | else: 197 | fullPath = defaultUrlStore & "/" & url 198 | 199 | result = fullPath.splitFile() 200 | 201 | proc componentAtUrl*(s: ConfigState, url: string, force: bool): ComponentInfo = 202 | ## Unlike the rest of this API, this call assumes the url is either: 203 | ## - A full https URL or; 204 | ## - A local filename, either as an absolutely path or relative to cwd. 205 | ## 206 | ## Here, unlike the other functions, we look for a file extension and chop 207 | ## it off. 208 | 209 | let 210 | (base, module, ext) = fullUrlToParts(url) 211 | 212 | result = s.fetchComponent(module, base, ext, force) 213 | 214 | s.loadComponent(result) 215 | 216 | proc loadComponentFromUrl*(s: ConfigState, url: string): ComponentInfo = 217 | return s.componentAtUrl(url, force = true) 218 | 219 | proc haveComponentFromUrl*(s: ConfigState, url: string): Option[ComponentInfo] = 220 | ## Does not fetch, only returns the component if we're using it. 221 | 222 | let 223 | (base, module, ext) = fullUrlToParts(url) 224 | 225 | if base.joinPath(module) notin s.components: 226 | return none(ComponentInfo) 227 | 228 | let component = s.getComponentReference(module, base) 229 | 230 | 231 | if component.source != "": 232 | result = some(component) 233 | else: 234 | result = none(ComponentInfo) 235 | 236 | component.fetchComponent(ext, force = false) 237 | 238 | 239 | proc loadCurrentComponent*(s: ConfigState) = 240 | s.loadComponent(s.currentComponent) 241 | 242 | template setParamValue*(s: ConfigState, 243 | component: ComponentInfo, 244 | paramName: string, 245 | value: Box, 246 | valueType: Con4mType, 247 | paramStore: untyped) = 248 | discard s.loadComponent(component) 249 | 250 | if paramName notin component.paramStore: 251 | raise newException(ValueError, "Parameter not found: " & paramName) 252 | 253 | let parameter = component.paramStore[paramName] 254 | 255 | if valueType.unify(parameter.defaultType).isBottom(): 256 | raise newException(ValueError, "Incompatable type for: " & paramName) 257 | 258 | parameter.value = some(value) 259 | 260 | proc setVariableParamValue*(s: ConfigState, 261 | component: ComponentInfo, 262 | paramName: string, 263 | value: Box, 264 | valueType: Con4mType) = 265 | s.setParamValue(component, paramName, value, valueType, varParams) 266 | 267 | 268 | proc setAttributeParamValue*(s: ConfigState, 269 | component: ComponentInfo, 270 | paramName: string, 271 | value: Box, 272 | valueType: Con4mType) = 273 | s.setParamValue(component, paramName, value, valueType, attrParams) 274 | 275 | proc setVariableParamValue*(s: ConfigState, 276 | urlKey: string, 277 | paramName: string, 278 | value: Box, 279 | valueType: Con4mType) = 280 | let component = s.getComponentReference(urlKey) 281 | s.setParamValue(component, paramName, value, valueType, varParams) 282 | 283 | proc setAttributeParamValue*(s: ConfigState, 284 | urlKey: string, 285 | paramName: string, 286 | value: Box, 287 | valueType: Con4mType) = 288 | let component = s.getComponentReference(urlKey) 289 | s.setParamValue(component, paramName, value, valueType, attrParams) 290 | 291 | proc setVariableParamValue*(s: ConfigState, 292 | componentName: string, 293 | location: string, 294 | paramName: string, 295 | value: Box, 296 | valueType: Con4mType) = 297 | let component = s.getComponentReference(componentName, location) 298 | s.setParamValue(component, paramName, value, valueType, varParams) 299 | 300 | proc setAttributeParamValue*(s: ConfigState, 301 | componentName: string, 302 | location: string, 303 | paramName: string, 304 | value: Box, 305 | valueType: Con4mType) = 306 | let component = s.getComponentReference(componentName, location) 307 | s.setParamValue(component, paramName, value, valueType, attrParams) 308 | 309 | proc getAllVariableParamInfo*(s: ConfigState, 310 | name, location: string): seq[ParameterInfo] = 311 | let component = s.getComponentReference(name, location) 312 | 313 | for _, v in component.varParams: 314 | result.add(v) 315 | 316 | proc getAllAttrParamInfo*(s: ConfigState, 317 | name, location: string): seq[ParameterInfo] = 318 | let component = s.getComponentReference(name, location) 319 | 320 | for _, v in component.attrParams: 321 | result.add(v) 322 | -------------------------------------------------------------------------------- /files/con4m/dollars.nim: -------------------------------------------------------------------------------- 1 | ## Functions to represent various data types as strings. For the 2 | ## things mapping to internal data structures, these are pretty much 3 | ## all just used for debugging. 4 | ## 5 | ## :Author: John Viega (john@crashoverride.com) 6 | ## :Copyright: 2022 7 | 8 | import options, strformat, tables, json, unicode, algorithm, nimutils, types, 9 | strcursor 10 | from strutils import join, repeat, toHex, toLowerAscii, replace 11 | 12 | 13 | # If you want to be able to reconstruct the original file, swap this 14 | # false to true. 15 | when false: 16 | proc `$`*(tok: Con4mToken): string = 17 | result = $(tok.cursor.slice(tok.startPos, tok.endPos)) 18 | 19 | else: 20 | proc `$`*(tok: Con4mToken): string = 21 | case tok.kind 22 | of TtStringLit: result = "\"" & tok.unescaped & "\"" 23 | of TtWhiteSpace: result = "~ws~" 24 | of TtNewLine: result = "~nl~" 25 | of TtSof: result = "~sof~" 26 | of TtEof: result = "~eof~" 27 | of ErrorTok: result = "~err~" 28 | of ErrorLongComment: result = "~unterm comment~" 29 | of ErrorStringLit: result = "~unterm string~" 30 | of ErrorCharLit: result = "~bad char lit~" 31 | of ErrorOtherLit: result = "~unterm other lit~" 32 | of TtOtherLit: 33 | result = "<<" & $(tok.cursor.slice(tok.startPos, tok.endPos)) & ">>" 34 | else: 35 | result = $(tok.cursor.slice(tok.startPos, tok.endPos)) 36 | 37 | template colorType(s: string): string = 38 | s.withColor("green") 39 | 40 | template colorLit(s: string): string = 41 | s.withColor("red") 42 | 43 | template colorNT(s: string): string = 44 | s.withColor("jazzberry") 45 | 46 | template colorT(s: string): string = 47 | s.withColor("orange") 48 | 49 | type ReverseTVInfo = ref object 50 | takenNames: seq[string] 51 | map: Table[int, string] 52 | nextIx: int 53 | 54 | const tvmap = "gtuvwxyznm" 55 | 56 | proc getTVName(t: Con4mType, ti: ReverseTVInfo): string = 57 | if t.localName.isSome(): 58 | result = "`" & t.localName.get() 59 | if result notin ti.takenNames: 60 | ti.map[t.varNum] = result 61 | ti.takenNames.add(result) 62 | else: 63 | if t.varNum in ti.map: 64 | result = ti.map[t.varNum] 65 | else: 66 | while true: 67 | var 68 | s = ti.nextIx.toHex().toLowerAscii() 69 | first = 0 70 | while s[first] == '0': 71 | first = first + 1 72 | for i in first ..< len(s): 73 | let n = int(s[i]) - 48 74 | if n < 10: 75 | s[i] = tvmap[n] 76 | ti.nextIx += 1 77 | s = "`" & s[first .. ^1] 78 | if s in ti.takenNames: continue 79 | ti.map[t.varNum] = s 80 | return s 81 | 82 | proc `$`*(t: Con4mType, tinfo: ReverseTVInfo = nil): string = 83 | let ti = if tinfo == nil: ReverseTVInfo(nextIx: 1) else: tinfo 84 | 85 | ## Prints a type object the way it should be written for input. 86 | ## Note that, in some contexts, 'func' might be required to 87 | ## distinguish a leading parenthesis from other expressions, 88 | ## as that is not printed out here. 89 | case t.kind 90 | of TypeBottom: return "void" 91 | of TypeString: return "string" 92 | of TypeBool: return "bool" 93 | of TypeInt: return "int" 94 | of TypeChar: return "char" 95 | of TypeFloat: return "float" 96 | of TypeDuration: return "Duration" 97 | of TypeIPAddr: return "IPAddr" 98 | of TypeCIDR: return "CIDR" 99 | of TypeSize: return "Size" 100 | of TypeDate: return "Date" 101 | of TypeTime: return "Time" 102 | of TypeDateTime: return "DateTime" 103 | of TypeList: return "list[" & `$`(t.itemType, ti) & "]" 104 | of TypeDict: 105 | return "dict[" & `$`(t.keyType, ti) & ", " & `$`(t.valType, ti) & "]" 106 | of TypeTuple: 107 | var s: seq[string] = @[] 108 | for item in t.itemTypes: s.add(`$`(item, ti)) 109 | return "tuple[" & join(s, ", ") & "]" 110 | of TypeTypeSpec: 111 | result = "typespec" 112 | if t.binding.kind == TypeTVar: 113 | if t.binding.localName.isSome() or t.binding.link.isSome(): 114 | result &= "[" & `$`(t.binding, ti) & "]" 115 | of TypeTVar: 116 | if t.link.isSome(): 117 | return `$`(t.link.get(), ti) 118 | else: 119 | if len(t.components) != 0: 120 | var parts: seq[string] = @[] 121 | for item in t.components: 122 | parts.add(`$`(item, ti)) 123 | return parts.join(" or ") 124 | else: 125 | return t.getTvName(ti) 126 | of TypeFunc: 127 | if t.noSpec: return "(...)" 128 | if t.params.len() == 0: 129 | return "() -> " & `$`(t.retType, ti) 130 | else: 131 | var paramTypes: seq[string] 132 | for item in t.params: 133 | paramTypes.add(`$`(item, ti)) 134 | if t.va: 135 | paramTypes[^1] = "*" & paramTypes[^1] 136 | return "(" & paramTypes.join(", ") & ") -> " & `$`(t.retType, ti) 137 | 138 | proc `$`*(c: CallbackObj): string = 139 | result = "func " & c.name 140 | if not c.getType().noSpec: result &= $(c.tInfo) 141 | 142 | proc formatNonTerm(self: Con4mNode, name: string, i: int): string 143 | 144 | proc formatTerm(self: Con4mNode, name: string, i: int): string = 145 | if not self.token.isSome(): 146 | return ' '.repeat(i) & name & " " 147 | 148 | result = ' '.repeat(i) & name & " " & colorLit($(self.token.get())) 149 | if self.typeInfo != nil: 150 | result = result & " -- type: " & colorLit($(self.typeInfo)) 151 | 152 | template fmtNt(name: string) = 153 | return self.formatNonTerm(colorNT(name), i) 154 | 155 | template fmtNtNamed(name: string) = 156 | return self.formatNonTerm(colorNT(name) & " " & 157 | colorT($(self.token.get())), i) 158 | 159 | template fmtT(name: string) = 160 | return self.formatTerm(colorT(name), i) & "\n" 161 | 162 | #template fmtTy(name: string) = 163 | # return self.formatNonTerm(colorType(name), i) 164 | 165 | proc `$`*(self: Con4mNode, i: int = 0): string = 166 | case self.kind 167 | of NodeBody: fmtNt("Body") 168 | of NodeParamBody: fmtNt("ParamBody") 169 | of NodeAttrAssign: fmtNt("AttrAssign") 170 | of NodeAttrSetLock: fmtNt("AttrSetLock") 171 | of NodeVarAssign: fmtNt("VarAssign") 172 | of NodeUnpack: fmtNt("Unpack") 173 | of NodeSection: fmtNt("Section") 174 | of NodeIfStmt: fmtNt("If Stmt") 175 | of NodeConditional: fmtNt("Conditional") 176 | of NodeElse: fmtNt("Else") 177 | of NodeFor: fmtNt("For") 178 | of NodeBreak: fmtT("Break") 179 | of NodeContinue: fmtT("Continue") 180 | of NodeReturn: fmtNt("Return") 181 | of NodeSimpLit: fmtT("Literal") 182 | of NodeUnary: fmtNtNamed("Unary") 183 | of NodeNot: fmtNt("Not") 184 | of NodeMember: fmtNt("Member") 185 | of NodeIndex: fmtNt("Index") 186 | of NodeCall: fmtNt("Call") 187 | of NodeActuals: fmtNt("Actuals") 188 | of NodeDictLit: fmtNt("DictLit") 189 | of NodeKVPair: fmtNt("KVPair") 190 | of NodeListLit: fmtNt("ListLit") 191 | of NodeTupleLit: fmtNt("TupleLit") 192 | of NodeCallbackLit: fmtNtNamed("CallbackLit") 193 | of NodeEnum: fmtNt("Enum") 194 | of NodeFuncDef: fmtNtNamed("Def") 195 | of NodeFormalList: fmtNt("Formals") 196 | of NodeType: fmtNt("Type") 197 | of NodeVarDecl: fmtNt("VarDecl") 198 | of NodeExportDecl: fmtNt("ExportDecl") 199 | of NodeVarSymNames: fmtNt("VarSymNames") 200 | of NodeUse: fmtNt("Use") 201 | of NodeParameter: fmtNt("Parameter") 202 | of NodeOr, NodeAnd, NodeNe, NodeCmp, NodeGte, NodeLte, NodeGt, 203 | NodeLt, NodePlus, NodeMinus, NodeMod, NodeMul, NodeDiv: 204 | fmtNt($(self.token.get())) 205 | of NodeIdentifier: fmtNtNamed("Identifier") 206 | 207 | proc formatNonTerm(self: Con4mNode, name: string, i: int): string = 208 | const 209 | typeTemplate = " -- type: {typeRepr}" 210 | mainTemplate = "{spaces}{name}{typeVal}\n" 211 | indentTemplate = "{result}{subitem}" 212 | let 213 | spaces = ' '.repeat(i) 214 | ti = self.typeInfo 215 | typeRepr = if ti == nil: "" else: colorType($(ti)) 216 | typeVal = if ti == nil: "" else: typeTemplate.fmt() 217 | 218 | result = mainTemplate.fmt() 219 | 220 | for item in self.children: 221 | let subitem = item.`$`(i + 2) 222 | result = indentTemplate.fmt() 223 | 224 | proc nativeSizeToStrBase2*(input: Con4mSize): string = 225 | var n, m: uint64 226 | 227 | if input == 0: return "0 bytes" 228 | else: result = "" 229 | 230 | m = input div 1099511627776'u64 231 | if m != 0: 232 | result = $(m) & "TB " 233 | n = input mod 1099511627776'u64 234 | else: 235 | n = input 236 | 237 | m = n div 1073741824 238 | if m != 0: 239 | result &= $(m) & "GB " 240 | n = n mod 1073741824 241 | 242 | m = n div 1048576 243 | if m != 0: 244 | result &= $(m) & "MB " 245 | n = n mod 1048576 246 | 247 | m = n div 1024 248 | if m != 0: 249 | result &= $(m) & "KB " 250 | n = n mod 1024 251 | 252 | if n != 0: 253 | result &= $(m) & "B" 254 | 255 | result = result.strip() 256 | 257 | proc nativeDurationToStr*(d: Con4mDuration): string = 258 | var 259 | usec = d mod 1000000 260 | numSec = d div 1000000 261 | n: uint64 262 | 263 | if d == 0: return "0 sec" 264 | else: result = "" 265 | 266 | n = numSec div (365 * 24 * 60 * 60) 267 | 268 | case n 269 | of 0: discard 270 | of 1: result = "1 year " 271 | else: result = $(n) & " years " 272 | 273 | numSec = numSec mod (365 * 24 * 60 * 60) 274 | 275 | n = numSec div (24 * 60 * 60) # number of days 276 | case n div 7 277 | of 0: discard 278 | of 1: result &= "1 week " 279 | else: result &= $(n) & " weeks " 280 | 281 | case n mod 7 282 | of 0: discard 283 | of 1: result &= " 1 day " 284 | else: result &= $(n mod 7) & " days " 285 | 286 | numSec = numSec mod (24 * 60 * 60) 287 | n = numSec div (60 * 60) 288 | 289 | case n 290 | of 0: discard 291 | of 1: result &= " 1 hour " 292 | else: result &= $(n) & " hours " 293 | 294 | numSec = numSec mod (60 * 60) 295 | n = numSec div 60 296 | 297 | case n 298 | of 0: discard 299 | of 1: result &= " 1 min " 300 | else: result &= $(n) & " mins " 301 | 302 | numSec = numSec mod 60 303 | 304 | case numSec 305 | of 0: discard 306 | of 1: result &= " 1 sec " 307 | else: result &= $(numSec) & " secs " 308 | 309 | n = usec div 1000 310 | if n != 0: result &= $(n) & " msec " 311 | 312 | usec = usec mod 1000 313 | if usec != 0: result &= $(usec) & " usec" 314 | 315 | result = result.strip() 316 | 317 | type ValToStrType* = enum vTDefault, vTNoLits 318 | 319 | proc oneArgToString*(t: Con4mType, 320 | b: Box, 321 | outType = vTDefault, 322 | lit = false): string = 323 | case t.resolveTypeVars().kind 324 | of TypeString: 325 | if outType != vtNoLits and lit: 326 | return "\"" & unpack[string](b) & "\"" 327 | else: 328 | return unpack[string](b) 329 | of TypeIPAddr, TypeCIDR, TypeDate, TypeTime, TypeDateTime: 330 | if outType != vtNoLits and lit: 331 | return "<<" & unpack[string](b) & ">>" 332 | else: 333 | return unpack[string](b) 334 | of TypeTypeSpec: 335 | return $(unpack[Con4mType](b)) 336 | of TypeFunc: 337 | let cb = unpack[CallbackObj](b) 338 | return "func " & cb.name & $(cb.tInfo) 339 | of TypeInt: 340 | return $(unpack[int](b)) 341 | of TypeChar: 342 | result = $(Rune(unpack[int](b))) 343 | if lit: 344 | # TODO: this really needs to do \... for non-printables. 345 | result = "'" & result & "'" 346 | 347 | of TypeFloat: 348 | return $(unpack[float](b)) 349 | of TypeBool: 350 | return $(unpack[bool](b)) 351 | of TypeDuration: 352 | return nativeDurationToStr(Con4mDuration(unpack[int](b))) 353 | of TypeSize: 354 | return nativeSizeToStrBase2(Con4mSize(unpack[int](b))) 355 | of TypeList: 356 | var 357 | strs: seq[string] = @[] 358 | l: seq[Box] = unpack[seq[Box]](b) 359 | for item in l: 360 | let itemType = t.itemType.resolveTypeVars() 361 | strs.add(oneArgToString(itemType, item, outType, true)) 362 | result = strs.join(", ") 363 | if outType == vTDefault: 364 | result = "[" & result & "]" 365 | of TypeTuple: 366 | var 367 | strs: seq[string] = @[] 368 | l: seq[Box] = unpack[seq[Box]](b) 369 | for i, item in l: 370 | let itemType = t.itemTypes[i].resolveTypeVars() 371 | strs.add(oneArgToString(itemType, item, outType, true)) 372 | result = strs.join(", ") 373 | if outType == vTDefault: 374 | result = "(" & result & ")" 375 | of TypeDict: 376 | var 377 | strs: seq[string] = @[] 378 | tbl: OrderedTableRef[Box, Box] = unpack[OrderedTableRef[Box, Box]](b) 379 | 380 | for k, v in tbl: 381 | let 382 | t1 = t.keyType.resolveTypeVars() 383 | t2 = t.valType.resolveTypeVars() 384 | ks = oneArgToString(t1, k, outType, true) 385 | vs = oneArgToString(t2, v, outType, true) 386 | strs.add(ks & " : " & vs) 387 | 388 | result = strs.join(", ") 389 | return "{" & result & "}" 390 | else: 391 | return "" 392 | 393 | proc reprOneLevel(self: AttrScope, inpath: seq[string]): string = 394 | var path = inpath & @[self.name] 395 | 396 | result = path.join(".") & "\n" 397 | var rows = @[@["Name", "Type", "Value"]] 398 | 399 | 400 | for k, v in self.contents: 401 | var row: seq[string] = @[] 402 | 403 | if v.isA(Attribute): 404 | var attr = v.get(Attribute) 405 | if attr.value.isSome(): 406 | let val = oneArgToString(attr.tInfo, attr.value.get()) 407 | row.add(@[attr.name, $(attr.tInfo), val]) 408 | else: 409 | row.add(@[attr.name, $(attr.tInfo), ""]) 410 | else: 411 | var sec = v.get(AttrScope) 412 | row.add(@[sec.name, "section", "n/a"]) 413 | rows.add(row) 414 | 415 | try: 416 | result &= rows.instantTableWithHeaders() 417 | except: 418 | result &= "

Empty table.

".stylizeHtml() 419 | 420 | for k, v in self.contents: 421 | if v.isA(AttrScope): 422 | var scope = v.get(AttrScope) 423 | result &= scope.reprOneLevel(path) 424 | 425 | proc `$`*(self: AttrScope): string = 426 | var parts: seq[string] = @[] 427 | return reprOneLevel(self, parts) 428 | 429 | proc `$`*(self: VarScope): string = 430 | result = "" 431 | 432 | if self.parent.isSome(): 433 | result = $(self.parent.get()) 434 | 435 | var rows = @[@["Name", "Type"]] 436 | for k, v in self.contents: 437 | rows.add(@[k, $(v.tInfo)]) 438 | 439 | result &= rows.instantTableWithHeaders() 440 | 441 | proc `<`(x, y: seq[string]): bool = 442 | if x[0] == y[0]: 443 | return x[1] < y[1] 444 | else: 445 | return x[0] < y[0] 446 | 447 | proc `$`*(f: FuncTableEntry): string = f.name & $(f.tInfo) 448 | 449 | proc `$`*(funcTable: Table[string, seq[FuncTableEntry]]): string = 450 | # Not technically a dollar, but hey. 451 | var rows: seq[seq[string]] = @[] 452 | for key, entrySet in funcTable: 453 | for entry in entrySet: 454 | rows.add(@[key, $(entry.tinfo), $(entry.kind)]) 455 | rows.sort() 456 | rows = @[@["Name", "Type", "Kind"]] & rows 457 | 458 | result &= rows.instantTableWithHeaders() 459 | -------------------------------------------------------------------------------- /files/con4m/errmsg.nim: -------------------------------------------------------------------------------- 1 | ## We're going to use the nimutils topic outputting system, publishing 2 | ## everything to a "con4m" topic. By default, we'll use the nimutils 3 | ## log-level system to decide what to publish. 4 | 5 | import tables, strutils, strformat, os, unicode, nimutils, nimutils/logging, 6 | types 7 | export getOrElse 8 | 9 | type 10 | InstInfo* = tuple[filename: string, line: int, column: int] 11 | C4Verbosity* = enum c4vBasic, c4vShowLoc, c4vTrace, c4vMax 12 | Con4mError* = object of CatchableError 13 | 14 | let 15 | con4mTopic* = registerTopic("con4m") 16 | `hook?` = configSink(getSinkImplementation("stderr").get(), 17 | "con4m-default", 18 | filters = @[MsgFilter(logLevelFilter), 19 | MsgFilter(logPrefixFilter)]) 20 | defaultCon4mHook* = `hook?`.get() 21 | 22 | var 23 | publishParams = { "loglevel" : $(llError) }.newOrderedTable() 24 | verbosity = c4vShowLoc 25 | curFileName: string 26 | 27 | proc setupTopStyle() = 28 | 29 | let s = newStyle(bgColor = "darkslategray", lpad=1) 30 | let r = newStyle(align = AlignC) 31 | 32 | perClassStyles["plain"] = s 33 | perClassStyles["woo"] = r 34 | styleMap["table"] = newStyle(borders=[BorderNone]) 35 | styleMap["thead"] = s 36 | styleMap["tbody"] = s 37 | styleMap["tfoot"] = s 38 | styleMap["td"] = s 39 | styleMap["tr"] = s 40 | styleMap["tr.even"] = s 41 | styleMap["tr.odd"] = s 42 | styleMap["th"] = s 43 | styleMap["caption"] = mergeStyles(styleMap["caption"], newStyle(bmargin=2)) 44 | 45 | 46 | proc setupBottomStyle() = 47 | let s = newStyle(bgColor = "mediumpurple") 48 | perClassStyles["plain"] = s 49 | styleMap["thead"] = s 50 | styleMap["table"] = newStyle(align = AlignC) 51 | styleMap["tbody"] = s 52 | styleMap["tfoot"] = s 53 | styleMap["td"] = s 54 | styleMap["tr"] = s 55 | styleMap["tr.even"] = s 56 | styleMap["tr.odd"] = s 57 | styleMap["th"] = newStyle(fgColor = "atomiclime", tmargin=1) 58 | 59 | 60 | proc formatTb(tb, throwinfo: string): string = 61 | var 62 | nimbleDirs: OrderedTable[string, string] 63 | row: string 64 | 65 | 66 | setupTopStyle() 67 | 68 | let lines = strutils.split(tb, "\n") 69 | for i, line in lines: 70 | if i == 0: 71 | result = "

" & line & "

\n" 72 | result &= "" 73 | result &= "" 74 | continue 75 | if len(line) == 0: 76 | continue 77 | let parts = line.split("/") 78 | if line[0] == '/' and "/.nimble/pkgs2/" in line: 79 | for j, item in parts: 80 | if item == "pkgs2": 81 | if parts[j+2] notin nimbleDirs: 82 | nimbleDirs[parts[j+2]] = parts[0 .. j+1].join("/") 83 | row = parts[j+2 ..< ^1].join("/") & "/" & parts[^1] & "" 84 | break 85 | else: 86 | row = parts[0 ..< ^1].join("/") & "/" & parts[^1] & "" 87 | 88 | let ix = row.find(' ') 89 | result &= "" 91 | 92 | result &= "" 93 | 94 | if throwinfo != "": 95 | result &= "
" & row[0 ..< ix] & "" & 90 | row[ix + 1 .. ^1] & "

" & throwinfo & 96 | "

" 97 | else: 98 | result &= "" 99 | 100 | result = result.stylizeHtml() 101 | 102 | if len(nimbleDirs) > 0: 103 | setupBottomStyle() 104 | var t2 = "

" & 105 | "" & 106 | "" & 107 | "" 108 | for k, v in nimbleDirs: 109 | t2 &= "" 111 | t2 &= "
PackageLocation
" & k & 110 | "" & v & "
Nimble packages used
" 112 | result &= t2.stylizeHtml() 113 | 114 | proc split*(str: seq[Rune], ch: Rune): seq[seq[Rune]] = 115 | var start = 0 116 | 117 | for i in 0 ..< len(str): 118 | if str[i] == ch: 119 | result.add(str[start ..< i]) 120 | start = i + 1 121 | 122 | result.add(str[start .. ^1]) 123 | 124 | proc setCon4mVerbosity*(level: C4Verbosity) = 125 | verbosity = level 126 | 127 | proc getCon4mVerbosity*(): C4Verbosity = 128 | return verbosity 129 | 130 | proc setCurrentFileName*(s: string) = 131 | curFileName = s 132 | 133 | proc getCurrentFileName*(): string = 134 | return curFileName 135 | 136 | proc formatCompilerError*(msg: string, 137 | t: Con4mToken, 138 | tb: string = "", 139 | ii: InstInfo): string = 140 | let 141 | me = getAppFileName().splitPath().tail 142 | 143 | result = me.withColor("red") & ": " & curFileName.withColor("jazzberry") & 144 | ": " 145 | 146 | if t != nil: 147 | result &= fmt"{t.lineNo}:{t.lineOffset+1}: " 148 | result &= "\n" & msg 149 | result &= result.stylize() 150 | 151 | if t != nil and verbosity in [c4vShowLoc, c4vMax]: 152 | let 153 | line = t.lineNo - 1 154 | offset = t.lineOffset + 1 155 | src = t.cursor.runes 156 | lines = src.split(Rune('\n')) 157 | pad = repeat((' '), offset + 1) 158 | 159 | result &= "\n" & " " & $(lines[line]) & "\n" 160 | result &= $(pad) & "^" 161 | 162 | if verbosity in [c4vTrace, c4vMax]: 163 | if tb != "": 164 | var throwinfo = "" 165 | if ii.line != 0: 166 | throwinfo &= "Exception thrown at: " 167 | throwinfo &= ii.filename & "(" & $(ii.line) & ":" & $(ii.column) & ")" 168 | result &= "\n" & formatTb(tb, throwinfo) 169 | 170 | proc rawPublish(level: LogLevel, msg: string) {.inline.} = 171 | publishParams["loglevel"] = $(level) 172 | discard publish(con4mTopic, msg & "\n", publishParams) 173 | 174 | proc fatal*(baseMsg: string, 175 | token: Con4mToken, 176 | st: string = "", 177 | ii: InstInfo = default(InstInfo)) = 178 | # 'Fatal' from con4m's perspective is throwing an exception that 179 | # returns to the caller. 180 | var msg: string 181 | 182 | if token == nil: 183 | msg = baseMsg 184 | elif token.lineNo == -1: 185 | msg = "(in code called by con4m): " & baseMsg 186 | else: 187 | msg = baseMsg 188 | 189 | raise newException(Con4mError, formatCompilerError(msg, token, st, ii)) 190 | 191 | template fatal*(msg: string, node: Con4mNode = nil) = 192 | var st = "" 193 | 194 | when not defined(release): 195 | st = getStackTrace() 196 | 197 | if node == nil: 198 | fatal(msg, Con4mToken(nil), st) 199 | else: 200 | fatal(msg, node.token.getOrElse(nil), st, instantiationInfo()) 201 | 202 | proc setCTrace*() = 203 | setLogLevel(llTrace) 204 | setCon4mVerbosity(c4vMax) 205 | rawPublish(llTrace, "debugging on.") 206 | 207 | proc ctrace*(msg: string) = 208 | if verbosity == c4vMax: 209 | rawPublish(llTrace, msg) 210 | -------------------------------------------------------------------------------- /files/con4m/legacy.nim: -------------------------------------------------------------------------------- 1 | ## Highest-level API for executing con4m. The macros provide more 2 | ## abstraction for stuff written in Nim. 3 | ## 4 | ## :Author: John Viega (john@crashoverride.com) 5 | ## :Copyright: 2022, 2023, Crash Override, Inc. 6 | 7 | import tables, options, streams, nimutils, strformat 8 | import errmsg, types, parse, treecheck, eval, spec, builtins, dollars 9 | 10 | proc newConfigState*(node: Con4mNode, 11 | spec: ConfigSpec = nil, 12 | addBuiltins: bool = true, 13 | exclude: openarray[int] = []): ConfigState = 14 | let attrRoot = AttrScope(parent: none(AttrScope), name: "<>") 15 | node.attrScope = attrRoot 16 | node.varScope = VarScope(parent: none(VarScope)) 17 | 18 | let specOpt = if spec == nil: none(ConfigSpec) else: some(spec) 19 | result = ConfigState(attrs: attrRoot, 20 | spec: specOpt, 21 | numExecutions: 0) 22 | 23 | node.attrScope.config = result 24 | 25 | if addBuiltins: 26 | result.addDefaultBuiltins(exclude) 27 | 28 | proc initRun*(n: Con4mNode, s: ConfigState) {.inline.} = 29 | var topFrame = RuntimeFrame() 30 | 31 | for k, sym in n.varScope.contents: 32 | if k notin topFrame: 33 | topFrame[k] = sym.value 34 | 35 | s.frames = @[topFrame] 36 | 37 | proc postRun(state: ConfigState) = 38 | if len(state.frames) > 0: 39 | for k, v in state.frames[0]: 40 | if k in state.keptGlobals: 41 | state.keptGlobals[k].value = v 42 | state.frames = @[] 43 | 44 | var showChecked = false 45 | proc setShowChecked*() = showChecked = true 46 | 47 | proc runBase(state: ConfigState, tree: Con4mNode, evalCtx: ConfigState): bool = 48 | if tree == nil: return false 49 | state.secondPass = false 50 | tree.checkTree(state) 51 | if showChecked: 52 | stderr.write(withColor("Entry point:\n", "cyan")) 53 | stderr.writeLine($tree) 54 | for item in state.moduleFuncDefs: 55 | if item.kind == FnBuiltIn: unreachable 56 | elif item.impl.isNone(): unreachable 57 | else: 58 | let typeStr = `$`(item.tInfo) 59 | stderr.writeLine(withColor(fmt"Function: {item.name}{typeStr}", "cyan")) 60 | stderr.writeLine($item.impl.get()) 61 | 62 | if state.spec.isSome(): 63 | state.basicSanityCheck(evalCtx) 64 | 65 | tree.initRun(state) 66 | try: 67 | ctrace(fmt"{getCurrentFileName()}: Beginning evaluation.") 68 | tree.evalNode(state) 69 | ctrace(fmt"{getCurrentFileName()}: Evaluation done.") 70 | finally: 71 | state.postRun() 72 | 73 | if state.spec.isSome(): 74 | state.validateState(evalCtx) 75 | 76 | state.numExecutions += 1 77 | 78 | return true 79 | 80 | proc firstRun*(stream: Stream, 81 | fileName: string, 82 | spec: ConfigSpec = nil, 83 | addBuiltins: bool = true, 84 | customFuncs: openarray[(string, BuiltinFn)] = [], 85 | exclude: openarray[int] = [], 86 | evalCtx: ConfigState = nil): (ConfigState, bool) = 87 | setCurrentFileName(fileName) 88 | # Parse throws an error if it doesn't succeed. 89 | var 90 | tree = parse(stream, filename) 91 | state = newConfigState(tree, spec, addBuiltins, exclude) 92 | 93 | for (sig, fn) in customFuncs: 94 | state.newBuiltIn(sig, fn) 95 | 96 | if state.runBase(tree, evalCtx): 97 | return (state, true) 98 | else: 99 | return (state, false) 100 | 101 | proc firstRun*(contents: string, 102 | fileName: string, 103 | spec: ConfigSpec = nil, 104 | addBuiltins: bool = true, 105 | customFuncs: openarray[(string, BuiltinFn)] = [], 106 | exclude: openarray[int] = [], 107 | evalCtx: ConfigState = nil): (ConfigState, bool) = 108 | return firstRun(newStringStream(contents), fileName, spec, addBuiltins, 109 | customFuncs, exclude, evalCtx) 110 | 111 | proc firstRun*(fileName: string, 112 | spec: ConfigSpec = nil, 113 | addBuiltins: bool = true, 114 | customFuncs: openarray[(string, BuiltinFn)] = [], 115 | exclude: openarray[int] = [], 116 | evalCtx: ConfigState = nil): (ConfigState, bool) = 117 | return firstRun(newFileStream(fileName, fmRead), fileName, spec, 118 | addBuiltins, customFuncs, exclude, evalCtx) 119 | 120 | proc stackConfig*(s: ConfigState, 121 | stream: Stream, 122 | fileName: string, 123 | evalCtx: ConfigState = nil): bool = 124 | setCurrentFileName(fileName) 125 | return s.runBase(parse(stream, fileName), evalCtx) 126 | 127 | proc stackConfig*(s: ConfigState, 128 | contents: string, 129 | filename: string, 130 | evalCtx: ConfigState = nil): bool = 131 | setCurrentFileName(filename) 132 | return s.runBase(parse(newStringStream(contents), filename), evalCtx) 133 | 134 | proc stackConfig*(s: ConfigState, 135 | filename: string, 136 | evalCtx: ConfigState = nil): bool = 137 | setCurrentFileName(filename) 138 | return s.runBase(parse(newFileStream(filename), filename), evalCtx) 139 | -------------------------------------------------------------------------------- /files/con4m/params.nim: -------------------------------------------------------------------------------- 1 | import types, treecheck, options, tables, nimutils, eval, dollars 2 | 3 | 4 | proc validateParameter*(state: ConfigState, param: ParameterInfo): 5 | Option[string] = 6 | if param.value.isNone(): 7 | return some("error: Must provide a valid value.") 8 | 9 | if param.validator.isSome(): 10 | let 11 | boxOpt = state.sCall(param.validator.get(), @[param.value.get()]) 12 | err = unpack[string](boxOpt.get()) 13 | 14 | if err != "": 15 | return some(err) 16 | 17 | return none(string) 18 | 19 | 20 | proc basicConfigureOneParam(state: ConfigState, 21 | component: ComponentInfo, 22 | param: ParameterInfo) = 23 | var 24 | boxOpt: Option[Box] 25 | default = "<>" 26 | 27 | if param.value.isSome(): 28 | boxOpt = param.value 29 | elif param.default.isSome(): 30 | boxOpt = param.default 31 | elif param.defaultCb.isSome(): 32 | boxOpt = state.sCall(param.defaultCb.get(), @[]) 33 | 34 | if boxOpt.isSome(): 35 | default = param.defaultType.oneArgToString(boxOpt.get()) 36 | default = stylize("Default is: " & default, ensureNl = false) 37 | let 38 | short = param.shortDoc.getOrElse("No description provided") 39 | long = param.doc.getOrElse("") 40 | intro = "Configuring: " & param.name & " -- " & 41 | "" & short & "\n" & long 42 | 43 | echo intro.stylizeMd() 44 | 45 | while true: 46 | if boxOpt.isSome(): 47 | echo default 48 | print("Press [enter] to accept default, or enter a value: ") 49 | else: 50 | print("Please enter a value: ", ensureNl = false) 51 | 52 | let line = stdin.readLine() 53 | 54 | if line != "": 55 | try: 56 | boxOpt = some(line.parseConstLiteral(param.defaultType)) 57 | except: 58 | echo(stylize(withColor("error: ", "red") & getCurrentExceptionMsg())) 59 | continue 60 | 61 | param.value = boxOpt 62 | 63 | let err = state.validateParameter(param) 64 | 65 | if err.isNone(): 66 | break 67 | 68 | echo(stylize(err.get())) 69 | 70 | proc basicConfigureParameters*(state: ConfigState, 71 | component: ComponentInfo, 72 | componentList: seq[ComponentInfo], 73 | nextPrompt = "Press [enter] to continue." 74 | ) = 75 | var shouldPause = false 76 | print("# Configuring Component: " & component.url) 77 | for subcomp in componentList: 78 | for name, param in subcomp.varParams: 79 | state.basicConfigureOneParam(subcomp, param) 80 | shouldPause = true 81 | 82 | for name, param in subcomp.attrParams: 83 | state.basicConfigureOneParam(subcomp, param) 84 | shouldPause = true 85 | print("# Finished configuration for " & component.url) 86 | if shouldPause: 87 | print(nextPrompt) 88 | discard stdin.readLine() 89 | -------------------------------------------------------------------------------- /files/con4m/run.nim: -------------------------------------------------------------------------------- 1 | ## 2 | ## :Author: John Viega (john@crashoverride.com) 3 | ## :Copyright: 2023, Crash Override, Inc. 4 | 5 | import types, options, st, streams, os, nimutils, json, strutils 6 | 7 | proc perLineWrap*(s: string, 8 | startingMaxLineWidth = -1, 9 | firstHangingIndent = 2, 10 | remainingIndents = 0, 11 | splitLongWords = true, 12 | seps: set[char] = Whitespace, 13 | newLine = "\n"): string = 14 | ## deprecated; no longer does what it says either. 15 | return s.indentWrap(startingMaxLineWidth, firstHangingIndent, splitLongWords, 16 | seps, newLine) 17 | # This stuff comes before some imports because Nim sucks at forward referencing. 18 | var config: AttrScope = nil 19 | 20 | proc setConfigState*(s: AttrScope) = 21 | config = s 22 | 23 | proc getConfigState(): AttrScope = 24 | if config == nil: config = AttrScope() 25 | return config 26 | 27 | proc getConf*[T](s: string): Option[T] = 28 | return getOpt[T](getConfigState(), s) 29 | 30 | import st, stack 31 | 32 | template cmdLineErrorOutput(msg: string) = 33 | let formatted = withColor( "error: ", "red").strip() & msg 34 | stderr.writeLine(formatted) 35 | quit(1) 36 | 37 | proc tryToOpen*(f: string): Stream = 38 | if f.fileExists(): 39 | try: 40 | return newFileStream(f) 41 | except: 42 | cmdLineErrorOutput("Error: could not open external config file " & 43 | "(permissions issue?)") 44 | else: 45 | cmdLineErrorOutput(f & ": file not found.") 46 | 47 | proc outputResults*(ctx: ConfigState) = 48 | case (getConf[string]("output_style")).get() 49 | of "pretty": echo $(ctx.attrs) 50 | of "raw": echo ctx.attrs.scopeToJson() 51 | of "json": 52 | let raw = ctx.attrs.scopeToJson() 53 | stderr.writeLine(withColor("Results:", "red")) 54 | stderr.writeLine(parseJson(raw).pretty()) 55 | 56 | else: discard 57 | 58 | template safeRun(stack: ConfigStack, backtrace = false) = 59 | try: 60 | stack.errored = false 61 | discard stack.run(backtrace) 62 | except: 63 | stack.errored = true 64 | getCurrentExceptionMsg().cmdLineErrorOutput() 65 | 66 | proc con4mRun*(files, specs: seq[string]) = 67 | let 68 | scope = if len(specs) > 1: srsBoth else: srsConfig 69 | stubs = (getConf[seq[string]]("stubs")).getOrElse(@[]) 70 | stack = newConfigStack().addSystemBuiltins(which=scope).addStubs(stubs) 71 | ctx = stack.run().get() 72 | whens = getConf[seq[string]]("output_when").get() 73 | 74 | for specfile in specs: 75 | let 76 | fname = specfile.resolvePath() 77 | stream = tryToOpen(fname) 78 | # TODO: add spec_whens 79 | stack.addSpecLoad(fname, stream) 80 | 81 | for i, filename in files: 82 | var addOut = false 83 | let 84 | fname = filename.resolvePath() 85 | stream = tryToOpen(fname) 86 | stack.addConfLoad(fname, stream) 87 | if "all" in whens: addOut = true 88 | else: 89 | if i == 0 and "first" in whens: addOut = true 90 | if i == len(files) - 1 and "last" in whens: addOut = true 91 | if "rest" in whens and i != 0 and i != len(files) - 1: addOut = true 92 | 93 | if addOut: 94 | stack.addCallback(outputResults) 95 | 96 | stack.safeRun(backtrace = true) 97 | 98 | proc specGenRun*(files: seq[string]) = 99 | let 100 | stubs = (getConf[seq[string]]("stubs")).getOrElse(@[]) 101 | stack = newConfigStack().addSystemBuiltins(which=srsValidation). 102 | addGetoptSpecLoad() 103 | 104 | for item in files: 105 | let 106 | fname = item.resolvePath() 107 | stream = tryToOpen(fname) 108 | 109 | stack.addSpecLoad(fname, stream) 110 | 111 | stack.addCodeGen(getConf[string]("language").get(), 112 | getConf[string]("output_file").getOrElse("")) 113 | stack.safeRun(backtrace = true) 114 | echo withColor("Code generation successful.", "atomiclime") 115 | -------------------------------------------------------------------------------- /files/con4m/strcursor.nim: -------------------------------------------------------------------------------- 1 | # I was using a Stream abstraction here, but streams won't marshall 2 | # and we will need them to to be able to support suspension and 3 | # resumption. 4 | # 5 | # Plus, I'd prefer to keep UTF32 instead of UTF8. 6 | import unicode, types 7 | 8 | proc newStringCursor*(s: string): StringCursor = 9 | result = StringCursor(runes: s.toRunes(), i: 0) 10 | 11 | template peek*(cursor: StringCursor): Rune = 12 | if cursor.i >= cursor.runes.len(): 13 | Rune(0) 14 | else: 15 | cursor.runes[cursor.i] 16 | 17 | proc read*(cursor: StringCursor): Rune = 18 | if cursor.i >= cursor.runes.len(): 19 | return Rune(0) 20 | else: 21 | result = cursor.runes[cursor.i] 22 | cursor.i += result.size() 23 | 24 | template advance*(cursor: StringCursor) = 25 | cursor.i += 1 26 | 27 | template getPosition*(cursor: StringCursor): int = cursor.i 28 | 29 | proc setPosition*(cursor: StringCursor, i: int) = 30 | cursor.i = i 31 | 32 | template slice*(cursor: StringCursor, startIx, endIx: int): seq[Rune] = 33 | cursor.runes[startIx ..< endIx] 34 | -------------------------------------------------------------------------------- /files/con4m/typecheck.nim: -------------------------------------------------------------------------------- 1 | ## Our implementation of the good ol' unifcation algorithm, with 2 | ## Con4m's current type rules codified. 3 | ## 4 | ## :Author: John Viega (john@crashoverride.com) 5 | ## :Copyright: 2022 6 | 7 | import types, tables, options, nimutils, dollars 8 | 9 | 10 | proc copyType*(t: Con4mType): Con4mType 11 | 12 | # This should only be called when we know that the type variable 13 | # is going to be unique for the context. It's mainly meant 14 | # for compile-time usage. 15 | proc newTypeVar*(num: int): Con4mType = 16 | return Con4mType(kind: TypeTVar, varNum: num) 17 | proc newTypeSpec*(): Con4mType = 18 | return Con4mType(kind: TypeTypeSpec, binding: newTypeVar()) 19 | proc newListType*(contained: Con4mType): Con4mType = 20 | return Con4mType(kind: TypeList, itemType: contained) 21 | proc newDictType*(keyType, valType: Con4mType): Con4mType = 22 | return Con4mType(kind: TypeDict, keyType: keyType, valType: valType) 23 | proc genericList*(): Con4mType = newListType(newTypeVar()) 24 | proc genericDict*(): Con4mType = newDictType(newTypeVar(), newTypeVar()) 25 | proc anyTuple*(): Con4mType = Con4mType(kind: TypeTuple, itemTypes: @[]) 26 | proc newProcType*(params: seq[Con4mType], 27 | retType: Con4mType, 28 | va: bool = false): Con4mType = 29 | if params.len() != 0: 30 | return Con4mType(kind: TypeFunc, params: params, va: va, retType: retType) 31 | else: 32 | return Con4mType(kind: TypeFunc, retType: retType) 33 | 34 | proc linkTypeVar(t1: Con4mType, t2: Con4mType) = 35 | if t1 == t2: return 36 | if t2.kind == TypeTVar: 37 | t2.linksin.add(t1) 38 | t1.link = some(t2) 39 | for item in t1.linksin: 40 | item.link = some(t2) 41 | t2.linksin.add(item) 42 | else: 43 | t1.link = some(t2) 44 | for item in t1.linksin: 45 | item.link = some(t2) 46 | 47 | t1.linksin = @[] 48 | 49 | proc getBaseType*(t: Con4mType): Con4mTypeKind = 50 | if t.kind == TypeTVar: 51 | if t.link.isSome(): return t.link.get().getBaseType() 52 | return TypeTVar 53 | else: 54 | return t.kind 55 | 56 | proc getBaseType*(node: Con4mNode): Con4mTypeKind = 57 | return node.typeInfo.getBaseType() 58 | 59 | proc isBottom*(t: Con4mType): bool = return t.kind == TypeBottom 60 | 61 | # Uncomment this if you need a trace of unify() calls, 62 | # rename unify below to unifyactual, and then 63 | # uncomment the debug wrapper for unify below. 64 | # proc unify*(param1: Con4mType, param2: Con4mType): Con4mType 65 | proc unify*(param1: Con4mType, param2: Con4mType): Con4mType {.inline.} = 66 | let 67 | t1 = param1.resolveTypeVars() 68 | t2 = param2.resolveTypeVars() 69 | 70 | if t2.kind == TypeTVar and t1.kind != TypeTVar: 71 | return t2.unify(t1) # autocast will be irrelevant here. 72 | 73 | case t1.kind 74 | # Just in case someone manages to clone a singleton, we 75 | # always check against the .kind field, instead of looking at 76 | # object equivolence for singletons (e.g., int, bottom) 77 | of TypeString, TypeBool, TypeInt, TypeFloat, TypeChar, TypeDuration, 78 | TypeIPAddr, TypeCIDR, TypeSize, TypeDate, TypeTime, TypeDateTime: 79 | if t2.kind == t1.kind: return t1 80 | return bottomType 81 | of TypeBottom: return bottomType 82 | of TypeTypeSpec: 83 | if t2.kind != TypeTypeSpec: 84 | return bottomType 85 | let 86 | bindtype1 = t1.binding.resolveTypeVars() 87 | bindtype2 = t2.binding.resolveTypeVars() 88 | 89 | # If both type specs are void, the unify returns bottom, since void 90 | # and bottom are generally the same thing. 91 | if bindtype1.kind == bindtype2.kind and bindtype1.isBottom(): 92 | return t1 93 | 94 | # Same thing is true if one side is a type variable that might bind to 95 | # a void. 96 | if bindtype1.isBottom() and bindtype2.kind == TypeTVar and 97 | len(bindtype2.components) == 0: 98 | return t1 99 | 100 | if bindtype2.isBottom() and bindtype1.kind == TypeTVar and 101 | len(bindtype1.components) == 0: 102 | return t2 103 | 104 | return bindtype1.unify(bindtype2) 105 | of TypeFunc: 106 | if t2.kind != TypeFunc: return bottomType 107 | if t2.noSpec: return t1 108 | if t1.noSpec: return t2 109 | var 110 | newParams: seq[Con4mType] 111 | newRet: Con4mType 112 | vaResult: bool 113 | 114 | # Actuals will never be varargs, so if we have two vararg 115 | # functions, it's only because we're trying to unify two formals. 116 | if ((not t1.va) and (not t2.va)) or (t1.va and t2.va): 117 | if t1.params.len() != t2.params.len(): return bottomType 118 | for i in 0 ..< t1.params.len(): 119 | let p = t1.params[i].unify(t2.params[i]) 120 | 121 | if p.kind == TypeBottom: return bottomType 122 | newParams.add(p) 123 | if t1.va: vaResult = true 124 | else: 125 | if t1.va: 126 | vaResult = true 127 | if t2.params.len() < t1.params.len() - 1: return bottomType 128 | for i in 0 ..< t1.params.len() - 1: 129 | let p = t1.params[i].unify(t2.params[i]) 130 | if p.kind == TypeBottom: return bottomType 131 | newParams.add(p) 132 | var vargType: Con4mType = t1.params[^1] 133 | for i in t1.params.len()-1 ..< t2.params.len(): 134 | vargType = vargType.unify(t2.params[i]) 135 | if vargType.kind == TypeBottom: return bottomType 136 | newParams.add(vargType) 137 | else: 138 | return t2.unify(t1) 139 | 140 | newRet = t1.retType.unify(t2.retType) 141 | if newRet.kind == TypeBottom: 142 | if not (t1.retType.kind in [TypeBottom, TypeTVar]) or 143 | not (t2.retType.kind in [TypeBottom, TypeTVar]): 144 | return bottomType 145 | 146 | return newProcType(newParams, newRet, vaResult) 147 | of TypeTuple: 148 | # If a tuple has no item types, then this is "any tuple", which 149 | # can only be specified in the context of an internal type 150 | # constraint. 151 | if t2.kind != TypeTuple: return bottomType 152 | elif len(t1.itemTypes) == 0: return t2 153 | elif len(t2.itemTypes) == 0: return t1 154 | elif len(t2.itemTypes) != len(t1.itemTypes): return bottomType 155 | result = Con4mType(kind: TypeTuple, itemTypes: @[]) 156 | for i, item in t1.itemTypes: 157 | let l = unify(item, t2.itemTypes[i]) 158 | if l.kind == TypeBottom: 159 | return bottomType 160 | result.itemTypes.add(l) 161 | return 162 | of TypeList: 163 | if t2.kind != TypeList: return bottomType 164 | let containedType = t1.itemType.unify(t2.itemType) 165 | if containedType == bottomType: return bottomType 166 | return newListType(containedType) 167 | of TypeDict: 168 | if t2.kind != TypeDict: return bottomType 169 | let kt = t1.keyType.unify(t2.keyType) 170 | let vt = t1.valType.unify(t2.valType) 171 | if kt.kind == TypeBottom or vt.kind == TypeBottom: return bottomType 172 | return newDictType(kt, vt) 173 | of TypeTVar: 174 | if t2.kind != TypeTVar: 175 | if len(t1.components) != 0: 176 | for item in t1.components: 177 | let v = item.copyType().unify(t2.copyType()) 178 | if not v.isBottom(): 179 | t1.linkTypeVar(t2) 180 | return t2 181 | return bottomType 182 | else: 183 | t1.linkTypeVar(t2) 184 | return t2 185 | elif len(t1.components) == 0: 186 | t1.linkTypeVar(t2) 187 | return t2 188 | elif len(t2.components) == 0: 189 | t2.linkTypeVar(t1) 190 | return t1 191 | # Here, both types are constrained, so we need to compute the 192 | # intersection. 193 | var foundTypes: seq[Con4mType] = @[] 194 | for t1item in t1.components: 195 | for t2item in t2.components: 196 | let res = t1item.unify(t2item) 197 | if not res.isBottom(): 198 | foundTypes.add(t2item) 199 | break 200 | case len(foundTypes) 201 | of 0: 202 | return bottomType 203 | of 1: 204 | t1.linkTypeVar(foundTypes[0]) 205 | t2.linkTypeVar(foundTypes[0]) 206 | else: 207 | t1.components = foundTypes 208 | t2.linkTypeVar(t1) 209 | return t1 210 | 211 | # If you need a trace of unify calls, follow the instructions above, then 212 | # uncomment the blow wrapper. 213 | # import strformat 214 | # proc unify*(param1: Con4mType, param2: Con4mType): Con4mType = 215 | # let 216 | # s1 = $(param1) 217 | # s2 = $(param2) 218 | # result = unifyActual(param1, param2) 219 | # echo fmt"{s1} ⋃ {s2} = {`$`(result)}" 220 | 221 | proc isBottom*(t1, t2: Con4mType): bool = return unify(t1, t2).isBottom() 222 | 223 | proc unify*(n2, n1: Con4mNode): Con4mType = 224 | return unify(n1.typeInfo, n2.typeInfo) 225 | 226 | proc isBottom*(n: Con4mNode): bool = return n.typeInfo.isBottom() 227 | 228 | proc isBottom*(n1, n2: Con4mNode): bool = 229 | return isBottom(n1.typeInfo, n2.typeInfo) 230 | 231 | proc isBottom*(n: Con4mNode, t: Con4mType): bool = 232 | return isBottom(n.typeInfo, t) 233 | 234 | proc hasTypeVar*(t: Con4mType): bool = 235 | case t.kind 236 | of TypeTVar, TypeTypeSpec: 237 | # Doesn't matter if it's a forward, return true and 238 | # get a clean copy! 239 | return true 240 | of TypeList: 241 | return t.itemType.hasTypeVar() 242 | of TypeDict: 243 | return t.keyType.hasTypeVar() or t.valType.hasTypeVar() 244 | of TypeTuple: 245 | for item in t.itemTypes: 246 | if item.hasTypeVar(): return true 247 | of TypeFunc: 248 | if t.nospec: return false 249 | for item in t.params: 250 | if item.hasTypeVar(): return true 251 | return t.retType.hasTypeVar() 252 | else: 253 | return false 254 | 255 | proc copyType*(t: Con4mType, cache: TableRef[int, Con4mType]): Con4mType = 256 | if not t.hasTypeVar(): return t 257 | 258 | case t.kind 259 | of TypeTVar: 260 | if t.varNum in cache: 261 | return cache[t.varNum] 262 | if t.link.isSome(): 263 | let n = t.varNum 264 | result = t.resolveTypeVars().copyType(cache) 265 | cache[n] = result 266 | else: 267 | result = newTypeVar(t.components) 268 | result.cycle = false 269 | cache[t.varNum] = result 270 | for item in t.components: 271 | result.components.add(item.copyType(cache)) 272 | if result.kind == TypeTVar: 273 | result.localName = t.localName 274 | of TypeTypeSpec: 275 | result = Con4mType(kind: TypeTypeSpec, binding: t.binding.copyType(cache)) 276 | of TypeList: 277 | result = Con4mType(kind: TypeList) 278 | result.itemType = t.itemType.copyType(cache) 279 | of TypeDict: 280 | result = Con4mType(kind: TypeDict) 281 | result.keyType = t.keyType.copyType(cache) 282 | result.valType = t.valType.copyType(cache) 283 | of TypeTuple: 284 | result = Con4mType(kind: TypeTuple) 285 | for param in t.itemTypes: 286 | result.itemTypes.add(param.copyType(cache)) 287 | of TypeFunc: 288 | result = Con4mType(kind: TypeFunc) 289 | for param in t.params: 290 | result.params.add(param.copyType(cache)) 291 | result.va = t.va 292 | result.retType = t.retType.copyType(cache) 293 | else: unreachable 294 | 295 | proc copyType*(t: Con4mType): Con4mType = 296 | var tVarCache = newTable[int, Con4mType]() 297 | return t.copytype(tVarCache) 298 | 299 | proc reprSig*(name: string, t: Con4mType): string = return name & $(t) 300 | 301 | 302 | proc getBoxType*(b: Box): Con4mType = 303 | case b.kind 304 | of MkStr: return stringType 305 | of MkInt: return intType 306 | of MkFloat: return floatType 307 | of MkBool: return boolType 308 | of MkSeq: 309 | var itemTypes: seq[Con4mType] 310 | let l = unpack[seq[Box]](b) 311 | 312 | if l.len() == 0: 313 | return newListType(newTypeVar()) 314 | 315 | for item in l: 316 | itemTypes.add(item.getBoxType()) 317 | for item in itemTypes[1..^1]: 318 | if item.unify(itemTypes[0]).isBottom(): 319 | return Con4mType(kind: TypeTuple, itemTypes: itemTypes) 320 | return newListType(itemTypes[0]) 321 | of MkTable: 322 | # This is a lie, but con4m doesn't have real objects, or a "Json" / Mixed 323 | # type, so we'll just continue to special case dicts. 324 | return newDictType(stringType, newTypeVar()) 325 | else: 326 | return newTypeVar() # The JSON "Null" can stand in for any type. 327 | 328 | proc checkAutoType*(b: Box, t: Con4mType): bool = 329 | return not b.getBoxType().unify(t).isBottom() 330 | -------------------------------------------------------------------------------- /files/deps/lib/linux-amd64/libc.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crashappsec/con4m/13e8f78982366246452789b85accbe3f9160ed12/files/deps/lib/linux-amd64/libc.a -------------------------------------------------------------------------------- /files/deps/lib/linux-amd64/libcmark-gfm-extensions.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crashappsec/con4m/13e8f78982366246452789b85accbe3f9160ed12/files/deps/lib/linux-amd64/libcmark-gfm-extensions.a -------------------------------------------------------------------------------- /files/deps/lib/linux-amd64/libcmark-gfm.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crashappsec/con4m/13e8f78982366246452789b85accbe3f9160ed12/files/deps/lib/linux-amd64/libcmark-gfm.a -------------------------------------------------------------------------------- /files/deps/lib/linux-amd64/libcrypt.a: -------------------------------------------------------------------------------- 1 | ! 2 | -------------------------------------------------------------------------------- /files/deps/lib/linux-amd64/libcrypto.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crashappsec/con4m/13e8f78982366246452789b85accbe3f9160ed12/files/deps/lib/linux-amd64/libcrypto.a -------------------------------------------------------------------------------- /files/deps/lib/linux-amd64/libdl.a: -------------------------------------------------------------------------------- 1 | ! 2 | -------------------------------------------------------------------------------- /files/deps/lib/linux-amd64/libgumbo.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crashappsec/con4m/13e8f78982366246452789b85accbe3f9160ed12/files/deps/lib/linux-amd64/libgumbo.a -------------------------------------------------------------------------------- /files/deps/lib/linux-amd64/libm.a: -------------------------------------------------------------------------------- 1 | ! 2 | -------------------------------------------------------------------------------- /files/deps/lib/linux-amd64/libpcre.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crashappsec/con4m/13e8f78982366246452789b85accbe3f9160ed12/files/deps/lib/linux-amd64/libpcre.a -------------------------------------------------------------------------------- /files/deps/lib/linux-amd64/libpthread.a: -------------------------------------------------------------------------------- 1 | ! 2 | -------------------------------------------------------------------------------- /files/deps/lib/linux-amd64/libresolv.a: -------------------------------------------------------------------------------- 1 | ! 2 | -------------------------------------------------------------------------------- /files/deps/lib/linux-amd64/librt.a: -------------------------------------------------------------------------------- 1 | ! 2 | -------------------------------------------------------------------------------- /files/deps/lib/linux-amd64/libssl.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crashappsec/con4m/13e8f78982366246452789b85accbe3f9160ed12/files/deps/lib/linux-amd64/libssl.a -------------------------------------------------------------------------------- /files/deps/lib/linux-amd64/libutil.a: -------------------------------------------------------------------------------- 1 | ! 2 | -------------------------------------------------------------------------------- /files/deps/lib/linux-amd64/libxnet.a: -------------------------------------------------------------------------------- 1 | ! 2 | -------------------------------------------------------------------------------- /files/deps/lib/linux-arm64/libc.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crashappsec/con4m/13e8f78982366246452789b85accbe3f9160ed12/files/deps/lib/linux-arm64/libc.a -------------------------------------------------------------------------------- /files/deps/lib/linux-arm64/libcrypt.a: -------------------------------------------------------------------------------- 1 | ! 2 | -------------------------------------------------------------------------------- /files/deps/lib/linux-arm64/libcrypto.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crashappsec/con4m/13e8f78982366246452789b85accbe3f9160ed12/files/deps/lib/linux-arm64/libcrypto.a -------------------------------------------------------------------------------- /files/deps/lib/linux-arm64/libdl.a: -------------------------------------------------------------------------------- 1 | ! 2 | -------------------------------------------------------------------------------- /files/deps/lib/linux-arm64/libgumbo.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crashappsec/con4m/13e8f78982366246452789b85accbe3f9160ed12/files/deps/lib/linux-arm64/libgumbo.a -------------------------------------------------------------------------------- /files/deps/lib/linux-arm64/libm.a: -------------------------------------------------------------------------------- 1 | ! 2 | -------------------------------------------------------------------------------- /files/deps/lib/linux-arm64/libpcre.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crashappsec/con4m/13e8f78982366246452789b85accbe3f9160ed12/files/deps/lib/linux-arm64/libpcre.a -------------------------------------------------------------------------------- /files/deps/lib/linux-arm64/libpthread.a: -------------------------------------------------------------------------------- 1 | ! 2 | -------------------------------------------------------------------------------- /files/deps/lib/linux-arm64/libresolv.a: -------------------------------------------------------------------------------- 1 | ! 2 | -------------------------------------------------------------------------------- /files/deps/lib/linux-arm64/librt.a: -------------------------------------------------------------------------------- 1 | ! 2 | -------------------------------------------------------------------------------- /files/deps/lib/linux-arm64/libssl.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crashappsec/con4m/13e8f78982366246452789b85accbe3f9160ed12/files/deps/lib/linux-arm64/libssl.a -------------------------------------------------------------------------------- /files/deps/lib/linux-arm64/libutil.a: -------------------------------------------------------------------------------- 1 | ! 2 | -------------------------------------------------------------------------------- /files/deps/lib/linux-arm64/libxnet.a: -------------------------------------------------------------------------------- 1 | ! 2 | -------------------------------------------------------------------------------- /files/deps/lib/macosx-amd64/libcrypto.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crashappsec/con4m/13e8f78982366246452789b85accbe3f9160ed12/files/deps/lib/macosx-amd64/libcrypto.a -------------------------------------------------------------------------------- /files/deps/lib/macosx-amd64/libpcre.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crashappsec/con4m/13e8f78982366246452789b85accbe3f9160ed12/files/deps/lib/macosx-amd64/libpcre.a -------------------------------------------------------------------------------- /files/deps/lib/macosx-amd64/libssl.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crashappsec/con4m/13e8f78982366246452789b85accbe3f9160ed12/files/deps/lib/macosx-amd64/libssl.a -------------------------------------------------------------------------------- /files/deps/lib/macosx-arm64/libcmark-gfm-extensions.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crashappsec/con4m/13e8f78982366246452789b85accbe3f9160ed12/files/deps/lib/macosx-arm64/libcmark-gfm-extensions.a -------------------------------------------------------------------------------- /files/deps/lib/macosx-arm64/libcmark-gfm.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crashappsec/con4m/13e8f78982366246452789b85accbe3f9160ed12/files/deps/lib/macosx-arm64/libcmark-gfm.a -------------------------------------------------------------------------------- /files/deps/lib/macosx-arm64/libcrypto.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crashappsec/con4m/13e8f78982366246452789b85accbe3f9160ed12/files/deps/lib/macosx-arm64/libcrypto.a -------------------------------------------------------------------------------- /files/deps/lib/macosx-arm64/libgumbo.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crashappsec/con4m/13e8f78982366246452789b85accbe3f9160ed12/files/deps/lib/macosx-arm64/libgumbo.a -------------------------------------------------------------------------------- /files/deps/lib/macosx-arm64/libpcre.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crashappsec/con4m/13e8f78982366246452789b85accbe3f9160ed12/files/deps/lib/macosx-arm64/libpcre.a -------------------------------------------------------------------------------- /files/deps/lib/macosx-arm64/libssl.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crashappsec/con4m/13e8f78982366246452789b85accbe3f9160ed12/files/deps/lib/macosx-arm64/libssl.a -------------------------------------------------------------------------------- /files/deps/macos/amd64/libcrypto.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crashappsec/con4m/13e8f78982366246452789b85accbe3f9160ed12/files/deps/macos/amd64/libcrypto.a -------------------------------------------------------------------------------- /files/deps/macos/amd64/libssl.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crashappsec/con4m/13e8f78982366246452789b85accbe3f9160ed12/files/deps/macos/amd64/libssl.a -------------------------------------------------------------------------------- /files/deps/macos/arm64/libcrypto.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crashappsec/con4m/13e8f78982366246452789b85accbe3f9160ed12/files/deps/macos/arm64/libcrypto.a -------------------------------------------------------------------------------- /files/deps/macos/arm64/libssl.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crashappsec/con4m/13e8f78982366246452789b85accbe3f9160ed12/files/deps/macos/arm64/libssl.a -------------------------------------------------------------------------------- /files/help/about.txt: -------------------------------------------------------------------------------- 1 | %{H About Con4m}% 2 | Con4m is open source under the Apache 2.0 license. 3 | 4 | Con4m was written by John Viega (john@crashoverride.com). 5 | 6 | Pull requests are welcome! We're especially happy for people to interface libcon4m to other languages, and to do the language-specific code generation. 7 | 8 | %{H More Information}% 9 | {bold}'{appName} help topics'{reset} for a list of all available help topics. 10 | -------------------------------------------------------------------------------- /files/help/api.txt: -------------------------------------------------------------------------------- 1 | %{H API Docs: Coming Soon.}% 2 | Until then, see either the Nim source code, or "con4m.h" for the C interface. 3 | -------------------------------------------------------------------------------- /files/help/builtins.txt: -------------------------------------------------------------------------------- 1 | %{H Con4m's Default Builtin Functions}% 2 | 3 | Note that, via API, individual calls can be excluded by providing the ID number associated with that function. See the API reference. 4 | %{T :<30: 5 | ID::Function::Description 6 | 1::bool(int) -> bool::Convert an integer to a true/false value. 7 | 2::bool(float) -> bool::Convert a float to a true/false value. 8 | 3::bool(string) -> bool:: Convert a string to a true/false value. 9 | 4::bool([@T]) -> bool::Convert a list of any type to a true/false value. 10 | 5::bool({@K : @V}) -> bool::Convert a dictionary of any type to a true/false value. 11 | 6::float(int) -> float::Convert an integer to a floating point representation. 12 | 7::int(float) -> int::Convert a float to an integer by truncating the decimal part. 13 | 8::$(@t) -> string::Convert any type into a string. 14 | 9::Duration(string) -> Duration::Convert a string into a Duration object. 15 | 10::IPAddr(string) -> IPAddr::Convert a string into an IPAddr object. 16 | 11::CIDR(string) -> CIDR::Convert a string into a CIDR object. 17 | 12::Size(string) -> Size::Convert a string into a size object. 18 | 13::Date(string) -> Date::Convert a string into a Date object. 19 | 14::Time(string) -> Time::Convert a string into a Time object. 20 | 15::DateTime(string) -> DateTime::Convert a string into a DateTime object. 21 | 16::to_usec(Duration) -> int::Extract the number of microseconds from a Duration object. 22 | 17::to_msec(Duration) -> int::Extract the number of milliseconds from a Duration object. 23 | 18::to_sec(Duration) -> int::Extract the number of seconds from a Duration object. 24 | 101::contains(string, string) -> bool::Returns true if the first argument contains second argument anywhere. 25 | 102::find(string, string) -> int::If the first string contains the second as a substring, returns the starting byte index of the first match, starting from 0. If the substring is not found, this returns -1. 26 | 103::len(string) -> int::Returns the number of bytes in a string 27 | 104::slice(string, int) -> string::Returns a new string that is a substring starting at the provided byte index, through the end of the string. As with languages like Python, negative values work, indexing from the back of the string. 28 | 105::slice(string, int, int) -> string::Returns a new string that is a substring starting at the first provided byte index, through the second provided index (not inclusive). As with languages like Python, negative values work, indexing from the back of the string. 29 | 106::split(string, string) -> [string]::Take the first string, and break it into a list containing all of the pieces that are separated by the second string, putting the results in a list 30 | 107::strip(string) -> string::Returns a second string, with any trailing or leading whitespace removed. 31 | 108::pad(string, int) -> string::Puts the specified number of spaces in front of each line of a string. 32 | 109::format(string) -> string:: Makes substitutions within a string, based on variables that are in scope. For the input string, anything inside braces {} will be treated as a specifier. You can access attributes that are out of scope by fully dotting from the top-level name. Note that all tags are currently part of the dotted name. You can use both attributes and variables in a specifier. Strings, bools, ints and floats are acceptable for specifiers, but lists and dictionaries are not. Note that there is currently no way to specify things like padding and alignment in a format specifier. If you want to insert an actual { or } character that shouldn't be part of a specifier, quote them by doubling them up (e.g., {{ to get a single left brace) 33 | 110::base64(string) -> string::Base64 encode a string. 34 | 111::base64_web(string) -> string::Base64 encode a string using the web-safe character scheme. 35 | 112::debase64(string) -> string::Decode a base64 encoded string (works with regular or web character sets) 36 | 113::hex(string) -> string::Hex-encode a string. 37 | 114::hex(int) -> string::Hex-encode an integer. 38 | 115::dehex(string) -> string::Decode a hex-encoded string. 39 | 116::sha256(string) -> string::Compute the SHA256 checksum of a value, returning a hex-encoded value. 40 | 117::sha512(string) -> string::Compute the SHA512 checksum of a value, returning a hex-encoded value. 41 | 118::upper(string) -> string::Convert all lower case code points in a string to upper case. 42 | 119::lower(string) -> string::Converts all upper case code points in a string to lower case. 43 | 201::len([@T]) -> int::Returns the number of items in a list. 44 | 202::len({@K : @V}) -> int:: Returns the number of items in a dictionary. 45 | 203::keys({@K : @V}) -> [@K]::Returns a list of the keys in a dictionary. 46 | 204::values({@K: @V}) -> [@V]::Returns a list of the values in a dictionary. 47 | 205::items({@K: @V}) -> [(@K, @V)]:: Returns a list of the key / value pairs in a dictionary. 48 | 206::contains([@T], @T) -> bool::Tests for presence of the second argument in the first argument. 49 | 207::contains({@K:@V}, @K) -> bool::Tests for presence of the second argument in the keys of the dictionary passed as the first argument. 50 | 208::set([@T], int, @T) -> [@T]:: Returns a new list based on the original list, where the item number passed in the second argument is replaced by the item passed in the third argument. Note that, in con4m, all data is immutable, so this will not update the original list, just return a new copy. 51 | 209::set({@K : @V}, @K, @V) -> {@K : @V}:: Returns a new dictionary that's the same as the first dictionary, except with the key specified in the second argument set to the value from the third argument. 52 | 210::delete([@T], @T) -> [@T])::Deletes any occurrences of an item from a list, returning a new list with any deletions. If the item is not in the list, a copy of the original list will be returned. The list is passed as the first argument, and the item to delete as the second. 53 | 211::delete({@k:@v}, @k) -> {@k:@v}::Deletes the key/value pair associated with a given key, returning a new dictionary. If the item is not in the dictionary, a copy of the original dictionary will be returned. The dict is passed as the first argument, and the key to delete as the second argument. 54 | 212::remove([@x], int) -> [@x]::Removes an item from a list by 0-based index, returning a copy of the list with the item removed. If the index is out of range, a copy of the original list is returned. 55 | 301::listDir() -> [string]::Returns a list of files in the current working directory 56 | 302::listDir(string) -> [string]::Returns a list of files in the specified directory. If the directory is invalid, no error is given; the results will be the same as if the directory were empty. 57 | 303::readFile(string) -> string::Returns the contents of the file. On error, this will return the empty string. 58 | 304::writeFile(string, string) -> bool::Writes, to the file name given in the first argument, the value of the string given in the second argument. Returns true if successful, false otherwise. 59 | 305::copyFile(string, string) -> bool::Copies the contents of the file specified by the first argument to the file specified by the second, creating the new file if necessary, overwriting it otherwise. Returns true if successful, false otherwise. 60 | 306::moveFile(string, string) -> bool::Moves the file specified by the first argument to the location specified by the second, overwriting any file, if present. Returns true if successful, false otherwise. 61 | 307::rmFile(string)->bool:: Removes the specified file, if allowed. Returns true if successful. 62 | 308::joinPath(string, string) -> string::Combines two pieces of a path in a way where you don't have to worry about extra slashes. 63 | 309::resolvePath(string) -> string::Turns a possibly relative path into an absolute path. This also expands tildes into home directories 64 | 310::splitPath(string) -> (string, string)::Separates out the final path component from the rest of the path, i.e., typically used to split out the file name from the remainder of the path. 65 | 311::cwd()->string::Returns the current working directory of the process. 66 | 312::chdir(string) -> bool::Changes the current working directory of the process. Returns true if successful. 67 | 313::mkdir(string) -> bool::Creates a directory, and returns true on success. 68 | 314::isDir(string) -> bool:: Returns true if the given file name exists at the time of the call, and is a directory. 69 | 315::isFile(string) -> bool:: Returns true if the given file name exists at the time of the call, and is a regular file. 70 | 316::isLink(string) -> bool::Returns true if the given file name exists at the time of the call, and is a link. 71 | 317::chmod(string, int) -> bool::Attempt to set the file permissions; returns true if successful. 72 | 318::fileLen(string) -> int::Returns the number of bytes in the specified file, or -1 if there is an error (e.g., no file, or not readable) 73 | 319::to_tmp_file(string, string) -> string::Creates a new temporary file, using significant entropy in the file name to avoid race conditions. The name of the temporary file is returned. The contents are populated by the string in the first parameter. The second parameter is an extra string that will be used as the end of the file name, so that it is recognizable on the file system (i.e., you set the extension). 74 | 401::echo(*string)::Output a list of strings... to stderr, NOT stdout. A newline is added at the end, but no spaces are added between arguments. 75 | 402::abort(string):: Stops the program immediately. 76 | 403::env() -> {string : string}::Returns a dictionary containing all environment variables and their contents. 77 | 404::env(string) -> string::Returns the value of a single environment variable. If not set, the empty string is returned. Note that it’s possible for environment variables to be set to an empty string, in which case use envExists() 78 | 405::envExists(string) -> bool::Returns true if the provided argument is a set environment variable, false otherwise. 79 | 406::setEnv(string, string) -> bool::Sets the environment variable specified in the first argument to the value specified in the second. Returns true if successful 80 | 407::getpid() -> int::Returns the current process ID 81 | 408::quote(string)->string::Takes a string, and quotes it in a way that's acceptable for passing to shells. 82 | 409::osname() -> string::A short string with the OS name 83 | 410::arch() -> string::A short string with the CPU architecture 84 | 411::program_args() -> [string]::The raw command line args the program received 85 | 412::program_path() -> string),::The full path to the executable running 86 | 413::program_name() -> string::The file name only for the executable 87 | 414::high() -> int::The highest value possible for an int 88 | 415::low() -> int::The lowest value possible for an int 89 | 416::rand() -> int:: Returns a cryptographically secure random number, uniformly distributed across all possible integers. 90 | 501::bitor(int, int) -> int::A bitwise OR 91 | 502::bitand(int, int) -> int::A bitwise AND 92 | 503::xor(int, int) -> int::The XOR operation 93 | 504::shl(int, int) -> int::Shift the bits of the first operand to the left by the amount specified in the second. 94 | 505::shr(int, int) -> int::Shift the bits of the first operand to the right by the amount specified in the second. 95 | 506::bitnot(int) -> int::Returns the binary inverse of an integer. 96 | 601::sections(string) -> [string]::This is primarily intended for a c42m specification; given a dotted con4m section name (the root is the empty string), returns a list with all the names of the con4m sections defined within that section, at the time of c42m validation. It's primarily meant for iterating through what "objects" are defined, so that you can, for instance, check to make sure references to object names in one part of a config are actually defined. This is also available during a con4m script's execution, but is far less useful there. 97 | 901::run(string) -> string::Runs a specified command, as if on the command line, returning the output (This is the same to the system() call in many languages). 98 | 902::system(string) -> (string, int)::Like system, runs a specified command as if on the command line, but returns a tuple consisting of the output and the exit code. Only available on Posix systems. 99 | 903::getuid() -> int:: Returns the current uid of the process. 100 | 904::geteuid() -> int::Returns the current euid of the process. Note, however, that, even if the process has euid 0, those permissions will be dropped before any call that runs a command or might modify the file system. 101 | 905::uname() -> [string]:: Returns five items, the sysname (the name of the operating system implementation), the nodename (the network name of this machine), the release level of the operating system, the version level of the operating system and the machine hardware platform. This is just proxying the uname() system call, but usually the version string (result[3]) will be everything-in-one. 102 | }% 103 | The function ID can be used to remove the function from a con4m runtime instance. Functions starting with 900 are Posix-specific, and may not be available on every platform (i.e., Windows) 104 | -------------------------------------------------------------------------------- /files/help/cmd.txt: -------------------------------------------------------------------------------- 1 | %{H Command line overview}% 2 | When running con4m the command line, by default, the 'compile' command runs, which executes con4m configuration files, and outputs the resulting configuration state, either as JSON, or in a pretty-printed table. 3 | 4 | If multiple config files are provided, they are executed 'stacked', run one after another, in the order in which they appear on the command-line. 5 | 6 | Configurations can be validated extensively if you provide a 'c42' spec file to validate against. See {bold}'{appName} help spec'{reset} for an example. 7 | 8 | Full con4m capabilities, particularly being able to run user-defined callbacks on-demand from your application, require calling con4m directly. Currently, con4m works natively with Nim, and provides a C API (libcon4m), which we intend to wrap for additional programming languages. 9 | 10 | The con4m compiler is capable of generating code to load native data structures and provide get/set interfaces for supported languages. To do this, you provide a schema in the form of a 'c42' spec file, and run the 'specgen' command. 11 | 12 | %{H FLAGS}% 13 | The following flags work for both compilation and spec generation: 14 | %{T 15 | Short::Long::Description 16 | -a::--attr-output=ARG::If the run successfully executes, this determines the style for outputting the resulting attribute space. Options are: "json", "pretty" (which gives tables), or "none". The default is "json", which prints attribute results to stdout, and only after all stacked configs are run. The "pretty" option will output state after each stacked config file runs. 17 | -k::--show-tokens::Show tokens after the parsing phase (if reached) 18 | -t::--show-parse-tree::Shows the UNTYPED parse tree after the parsing phase (if reached), but before the checking phase. 19 | -x::--show-checked-tree::It's Christmas! Show the typed parse tree after the checking phase. 20 | -F::--show-funcs::Show the function table once execution ends successfully. 21 | -C::--no-color::Disable ANSI colors in output. This overrides the presence or absence of a 'NO_COLORS' environment variable. 22 | -d::--debug::Turn on any custom runtime tracing, including stack traces for errors 23 | ::--tokenize::Alias for --phase=tokenize 24 | ::--parse::Alias for --phase=parse 25 | ::--check::Alias for --phase=check 26 | ::--pretty::Alias for --attr-output=pretty 27 | ::--none::Alias for --attr-output=none 28 | -h::--help::This help message 29 | }% 30 | 31 | Additionally, the 'compile' command takes the following flags: 32 | %{T 33 | Short::Long::Description 34 | -p::--phase=ARG::Stop the parse after the given phase. Valid values are 'tokenize', 'parse', 'check', 'eval'; 'eval' is the default. 35 | -s::--spec=ARG::Before beginning, load a con4m 'c42spec' file, and then apply it to the c4m files that are parsed, to do additional validation. 36 | }% 37 | 38 | The 'specgen' command takes only a single argument, the c42 specification file to validate and generate code for. This command takes the following flags: 39 | %{T 40 | Short::Long::Description 41 | -l::--language=ARG::The language for which to generate output. Currently, this MUST be provided. Valid values are "none" (validates only), and "nim". C is coming soon. 42 | -o::--output-file=ARG::Instead of writing to stdout, writes the generated code to the specified file. Ignored if the language is set to "none". 43 | }% 44 | 45 | %{H More Information}% 46 | {bold}'{appName} help overview'{reset} for an overview of the language. 47 | {bold}'{appName} help specs'{reset} for details on working with Con4m specifications for additional checking. 48 | {bold}'{appName} help con4m'{reset} for an overview of the con4m config file format (and language). 49 | {bold}'{appName} help topics'{reset} for a list of all available help topics. 50 | -------------------------------------------------------------------------------- /files/help/con4m.txt: -------------------------------------------------------------------------------- 1 | %{H Writing Con4m Config Files}% 2 | 3 | Con4m is meant to be familiar to anyone who has ever messed with a configuration file. But, it also makes it easy to make configuration decisions at the time the program runs (and at some point, dynamically if desired). 4 | 5 | You can set global configuration values, and add named sections that can be tagged, and have attributes. For instance: 6 | 7 | %{c 8 | git_user: "john@crashoverride.com" 9 | repository con4m { 10 | URI: "https://www.github.com/crashappsec/con4m.git" 11 | key_file: "~/.ssh/con4mKey" 12 | } 13 | repository chalk { 14 | URI: "https://www.github.com/crashappsec/chalk.git" 15 | key_file: "~/.ssh/chalkKey" 16 | } 17 | }% 18 | 19 | In Con4m, attribute names are standard unicode identifiers. The right hand side of an attribute is not free-form. Strings need to be put in quotes (and they do accept unicode escape sequences, etc). 20 | 21 | The section name (here, just repository), is followed by an optional tag. But this might depend on the context—- most uses of Con4m will enforce a config file schema. We think of sections with tags as "objects" where there might be more than one. Whereas, sections without tags are "singletons". For instance, a c42 spec has a singleton named "root" you provide to specify what can be provided at the top level. For example, here's a minimal c42 spec, itself written in con4m: 22 | %{c 23 | root { 24 | user_def_ok: false 25 | field color { 26 | type: "bool" 27 | required: true 28 | } 29 | } 30 | }% 31 | %{h Builtin function calls }% 32 | One feature of Con4m is that can make function calls, for instance to run other shell commands, or output text: 33 | %{c 34 | text := run("ls -l") 35 | echo(text) 36 | This will output (on my machine): 37 | 38 | total 712 39 | drwx------@ 3 viega staff 96 Oct 29 20:02 Applications 40 | drwx------+ 8 viega staff 256 Dec 5 18:48 Desktop 41 | drwx------+ 21 viega staff 672 Nov 7 14:56 Documents 42 | drwx------@ 537 viega staff 17184 Dec 6 20:55 Downloads 43 | ... 44 | }% 45 | Con4m provides several built-in calls, and will be adding more (see the current list at {bold}'{appName} help builtins'{reset}). Individual applications can add or remove them easily, though, so it’s good to check the documentation for any particular application. 46 | 47 | You can also write and call your own functions in the config file, which is discussed below. 48 | 49 | One particularly useful call is env(). It gives you the ability to override configuration decisions encoded in the file, without changing the file. But, the program doesn’t have to add support for environment variables you have to hunt around to find… you get to create the environment variables you need, and name them what you like, thanks to the if statement: 50 | 51 | %{h If statements }% 52 | The Con4m if statement supports arbitrary elif branches, and a final else branch. Obviously, different branches may end up not setting the same attributes. Thats okay… the application gets to decide if a configuration is valid or not, and usually many options will have default values, or be truly optional. 53 | 54 | %{c 55 | default_compiler_flags: "-O2" 56 | if env("CON4M_DEBUG") { 57 | default_compiler_flags: "-DDEBUG -O0" # replaces the other assignment 58 | } 59 | elif env("CON4M_ADD_FPIC") { 60 | # Note that format({}) implements Python-like formatters with the 61 | # local environment. 62 | default_compiler_flags: format("{default_compiler_flags} -fPIC") 63 | } 64 | }% 65 | 66 | It’s important to know that loop conditions need to evaluate to either true or false. You can use or and and (|| and && also work) as well, but again, each side must evaluate to true or false. Any other data type can be converted to true or false with a builtin conversion call, bool(). This requirement might be annoying to people who aren’t used to it, but results in more robust code in our experience. 67 | 68 | %{h Attributes vs. Variables }% 69 | You might have noticed, in the first example, we set config file keys with a colon. And, we could also have used an equals sign. People just slapping together simple config files shouldn’t have to work too hard on syntax to get it right, and both are familiar, so both should work. 70 | 71 | However, as mentioned above, one of the advantages of Con4m from an application writer’s point of view is that they can write a simple schema that will still sanity check the application, making sure that the right attributes are present, and no more. They need to be able to make sure that the config file writer does not pollute the namespace with junk! 72 | 73 | But, if you’re doing any sort of transformation to set attributes, you will probably want helper variables that do NOT get presented as part of the program’s configuration attributes. For that, we use the so-called "tusk" operator, used in languages like go. We did actually see this above: 74 | %{c 75 | text := run("ls -l") 76 | }% 77 | This command will create a variable called text that’s available in the execution environment, but is not used by the program you’re configuring. And, when we add runtime callbacks into the language (so that you can configure based on dynamic runtime information), the default will be that the variable state will still be available. 78 | %{h For Loops}% 79 | Con4m currently doesn’t allow indefinite loops, but for loops work, and so do break and continue: 80 | %{c 81 | path := env("PATH") 82 | desired := "~" 83 | found = false 84 | parts := split(path, ":") 85 | for i from 0 to len(parts) { 86 | if parts[i] == "" { 87 | continue 88 | } 89 | elif parts[i] == desired { 90 | break 91 | } 92 | echo ("Not what I'm looking for: ", parts[i]) 93 | } 94 | if found { 95 | echo("Found it!") 96 | } 97 | }% 98 | 99 | Some things to note here: 100 | %{- 101 | We currently only support for i from n to m syntax. The numbers can go up or down, but we don’t support steps, or iterating explicitly over arrays or dictionaries. This will probably change. 102 | The user is not allowed to modify the value of the index variable themselves. It must be an identifier (not a complex expression), and it can only be incremented by the system, helping to prevent infinite loops. 103 | The desire to ensure termination is why there currently isn’t a while loop. We will probably add it at some point, but in conjunction with a ceiling on overall config file execution time. 104 | The end value in the range is evaluated BEFORE the loop body runs, not on every iteration. 105 | }% 106 | %{h Data types and Static Typing }% 107 | Con4m has lists and dictionaries, with syntax like you’d expect: 108 | %{c 109 | my_path = ["/home/viega/bin", "/usr/sbin", "/usr/bin", "."] 110 | user_priorities = { "john@crashoverride.com" : 5, 111 | "mark@crashoverride.com" : 1, 112 | "theo@crashoverride.com" : 10 113 | } 114 | }% 115 | You can access items with square brackets: 116 | %{c 117 | for i from 0 to len(my_path) { 118 | echo(my_path[i]) 119 | } 120 | 121 | echo user_priorities["john@crashoverride.com"] 122 | }% 123 | 124 | Note that list items must all have the same type. Similarly, dictionary keys must always have the same type, as must dictionary key and values (though it's fine for keys and values to have different types). 125 | 126 | Con4m does significant static type checking. Once an attribute or variable is assigned a type, any inconsistencies will result in a compile-time (config load time) error. All types are currently inferred from the context -- you do not have to declare types. Additionally, specific config file formats might enforce additional value checking before loading your configuration. 127 | 128 | If there's no schema governing the types of values, you can set individual attributes to any type you want. They just cannot change. And, most uses of Con4m {bold}will{reset} specify a schema, meaning that after your config finishes loading, it will be generally checked against the specified types, and any incompatibility will again produce an error (though the application could choose to ignore it). 129 | 130 | Con4m also has tuple types. For instance, the built in system() function acts like run(), except that it returns a tuple, the first value containing the output of the command, the second tuple containing the exit value: 131 | %{c 132 | output, exitval = system("mycommand -x") 133 | 134 | if exitval != 0 { 135 | echo("That didn't work :(") 136 | } else { 137 | echo(output) 138 | } 139 | }% 140 | %{h Functions}% 141 | Con4m does support user-defined functions. You can create and call a function just for your internal use, or with the intent of having the runtime call it (in which case, the runtime needs to know to call it-- generally they will provide guidance about what callbacks to provide, or how to register your own). 142 | 143 | All the code in your config file NOT in a callback will run when the config file loads. Callbacks run after, whenever the program decides it needs to call it. To that end, you can only define callbacks with name and parameters that are actually used by the application. 144 | 145 | Certainly, you can call your functions yourself: 146 | %{c 147 | func fact(f) { 148 | result := 1 149 | for i from 1 to f { 150 | result := result * i 151 | } 152 | } 153 | 154 | for i from 1 to 10 { 155 | x := fact(i) 156 | echo (format("fact({i}) = {x}")) 157 | } 158 | }% 159 | 160 | Some things to note about functions: 161 | 162 | %{- 163 | You CAN call global variables inside functions or callbacks, but you first need to export them with the export operator ($), which you put in front of a global variable name on its first assignment. 164 | You do not need to worry about calling a function in a place in the file before you define it. The compiler figures this out. 165 | Recursive functions are available by default, but they can be turned off at the time the program using con4m is compiled. 166 | Currently, users can NOT declare varargs functions (though, builtins can). We may choose to add this, but do not right now. 167 | We do not support default arguments. 168 | Generally, the type inferencing system can resolve types at compile time for everything based on usage. The only exception is when you're calling an overloaded builtin, where one or more of the parameters is never used in any other context. Con4m requires binding to a single call implementation; it doesn't do any dynamic dispatch. Therefore, you can provide a type hint for local variables. You can, when declaring the function arguments, add a type using a colon. For instance: 169 | }% 170 | %{c 171 | func (f: string) { 172 | ... 173 | } 174 | }% 175 | 176 | Or, you can use the 'var' keyword to explicitly state types of any 177 | variable (this does not work for attributes: 178 | %{c 179 | var n: int 180 | }% 181 | 182 | %{h Attribute locking }% 183 | You can 'lock' attributes. For instance, your app might use con4m to set some values that the user shouldn't be able to change, once you hit production. You can apply a ~ in front of the attribute assignment, or programatically lock it via API at any time. This is also useful from a system configuration file that would like to keep users from shooting themselves in the foot by overriding the wrong things in the inherited configuration. 184 | 185 | %{H More Information}% 186 | {bold}'{appName} help builtins'{reset} for a list of the default builtin functions. 187 | {bold}'{appName} help reference'{reset} for con4m's syntax reference. 188 | -------------------------------------------------------------------------------- /files/help/help.txt: -------------------------------------------------------------------------------- 1 | %{H Usage: {appName} [FLAGS] FILE ... }% 2 | %{H {appName} specgen [FLAGS] FILE }% 3 | %{H {appName} help [TOPIC] }% 4 | 5 | Con4m makes it EASY to build custom configuration files for your application that are fully validated to your specification. Your users can write in a simple, familiar format, but have access a lot more power, if needed. They can even easily write custom callbacks for you to call from your own environment. Plus, Con4m makes it easy to 'stack' configurations on top of each other, to customize environment variables, to reload configurations at runtime, etc. 6 | 7 | Con4m is accessible via command or API (currently from C or Nim, with Python and Go coming soon). 8 | 9 | %{H More information}% 10 | {bold}'{appName} help cmd'{reset} for an overview of the command line functionality, including arguments. 11 | {bold}'{appName} help overview'{reset} for an overview of the language. 12 | {bold}'{appName} help topics'{reset} for a list of all available help topics. 13 | -------------------------------------------------------------------------------- /files/help/installing.txt: -------------------------------------------------------------------------------- 1 | %{H Installing Con4m}% 2 | 3 | Con4m is distributed from {bold}https://github.com/crashappsec/con4m{reset}. 4 | 5 | Currently, it is in pre-release, so needs a Nim compiler to build the con4m compiler and libcon4m.a. 6 | 7 | %{h Building from source}% 8 | Con4m is currently written against Nim 1.6.10, which can easil be installed via 'choosenim'. On most OSes, this can be installed from the command line with: 9 | 10 | %{c 11 | curl https://nim-lang.org/choosenim/init.sh -sSf | sh 12 | }% 13 | 14 | For windows, visit: https://github.com/dom96/choosenim/releases 15 | 16 | Once Nim is installed, you will need to update the package manager. Run: 17 | %{c 18 | nimble install nimble 19 | }% 20 | 21 | You can then simply run: 22 | 23 | %{c 24 | nimble install https://github.com/crashappsec/con4m 25 | }% 26 | 27 | This will put the con4m binary in your Nim path (usually, ~/.nimble/bin/con4m), and will place libcon4m.a in ~/lib/libcon4m.a 28 | 29 | %{H More Information}% 30 | {bold}'{appName} help topics'{reset} for a list of all available help topics. 31 | -------------------------------------------------------------------------------- /files/help/overview.txt: -------------------------------------------------------------------------------- 1 | %{H Con4m Overview}% 2 | 3 | We got tired of building mini-DSLs in YAML, especially since YAML has many syntactic quirks that make it bad for such things. 4 | 5 | Con4m gives you an Apache-like configuration file format, while meeting any scripting needs your users might have. Con4m will validate configuration files before loading them, even making sure the types and values all what YOU need them to be, if your provide a brief specification defining the schema you want for your config files. 6 | 7 | Con4m also allows you to 'stack' configuration files. For instance, the app can load an internal default configuration hardcoded into the program, then layer a system-level config over it, then layer a local config on top. 8 | 9 | After the configuration file loads, you can call any user-defined functions provided, if your application might need feedback from the user after configuration loads. 10 | 11 | You can also create your own builtin functions to make available to users who use the scripting capabilities. Con4m does provides a bunch of builtins, but you can selectively disable them if you so choose. 12 | 13 | %{H Basic Example}% 14 | 15 | Let’s imagine the user has provided a config file like this: 16 | %{c 17 | use_color: false 18 | 19 | host localhost { 20 | ip: "127.0.0.1" 21 | port: 8080 22 | } 23 | host workstation { 24 | port: 8080 25 | if env("CUSTOM_VAR") != "" { 26 | ip: env("CUSTOM_VAR") 27 | } 28 | else { 29 | ip: "10.12.1.10" 30 | } 31 | } 32 | }% 33 | 34 | In this example, the conditional runs when the config file is evaluated (if something needs to be evaluated dynamically, you can do that with a callback). 35 | 36 | Con4m provides a number of builtin functions like env(), which makes it easy for you to check environment variables, but also for your users to customize environment variables to suit their needs. You can easily provide your own built-in functions. 37 | 38 | Let’s say the application writer has loaded this configuration file into the variable s. She may then write the following c42 spec: 39 | 40 | %{c 41 | object host { 42 | field ip { 43 | type: "string" 44 | required: true 45 | } 46 | 47 | field port { 48 | type: "int" 49 | required: true 50 | } 51 | 52 | field use_tls { 53 | type: "bool" 54 | default: true 55 | } 56 | } 57 | }% 58 | When you call Con4m with the above spec, asking it to load a user's configuration file, the following happens: 59 | %{# 60 | The spec file loads. 61 | The user's configuration is checked for syntax and type safety. 62 | The user's config is evaluated. 63 | The user's config file is type checked against the type information provided in the spec. You can also provide additional validation constraints, like forcing strings to be from a particular set of values, or having integers be in a range. Whenever these constraints are violated, the user gets a descriptive error message. 64 | If the user doesn't provide information, default values will be loaded from your spec. If you didn't provide a value, but the field is required, then the user gets an appropriate error. 65 | You then get an easy API for querying and setting these values as your code runs. And, you can call back into the user's config via callback whenever needed. 66 | }% 67 | 68 | %{H More Information}% 69 | {bold}'{appName} help con4m'{reset} for an introduction to con4m's syntax. 70 | {bold}'{appName} help topics'{reset} for a list of all available help topics. 71 | -------------------------------------------------------------------------------- /files/help/reference.txt: -------------------------------------------------------------------------------- 1 | %{H Con4m Language syntax reference }% 2 | Below is a syntax reference for the Con4m language. The syntax is meant to be simple enough that anyone with even minimal programming experience should be able to pick up. And, for those people who just want to edit key/value configuration items without writing any code, that should be equally simple and familiar. 3 | 4 | The language is designed to be fast, and currently does not have any features that might lead to an infinite loop (or infinite recursion). 5 | 6 | %{h EBNF specification }% 7 | Major lexical elements are are all-uppercase, and detailed below. Minor ones are inlined, in double quotes. Note that all comment and white space tokens, other than newlines, are always ignored. 8 | Newlines are ignored in most expression contexts (they’re only used to separate statements where there would otherwise be ambiguity). 9 | %{c 10 | top ::= (sectBodyItems | enum | fnOrCallback) * 11 | body ::= sectBodyItems * 12 | coreBodyItems ::= ifStmt | forStmt | continueStmt | breakStmt | returnStmt | 13 | useStmt | fromStmt | parameterDecl | exportStmt | varStmt | 14 | assignment expression (NL|";")+ 15 | assignment ::= accessExpr ("," accessExpr)* eqOp expression (NL|";")+ 16 | eqOp ::= ("="|":"|":=") 17 | sectBodyItems ::= coreBodyItems | section 18 | enum ::= "enum" ID ("," ID)* 19 | 20 | section ::= ID (STR | ID)? "{" body "}" 21 | ifStmt ::= "if" expression "{" body "}" 22 | ("elif" expression "{" body "}")* 23 | ("else" expression "{" body" "}")? 24 | paramDefault ::= "default" ":" expr 25 | paramValidate ::= "validate" ":" expr 26 | paramBody ::= (STR (STR?))? (paramDefault?|paramValidate?)| 27 | (paramValidate?|paramDefault?) 28 | parameterDecl ::= "parameter" (("var" ID) | ID ("." ID)*) "{" paramBody "}" 29 | forStmt ::= "for" ID "from" expression "to" expression "{" body "}" 30 | useStmt ::= "use" ID 31 | fromStmt ::= "from" STR useStmt 32 | continueStmt ::= "continue" (";")? 33 | breakStmt ::= "break" (";")? 34 | returnStmt ::= "return" expression? (";")? 35 | fnOrCallback ::= ("func" | "callback") ID formalSpec fnBody 36 | formalSpec ::= "(" (paramSpec? ("," paramSpec)* ")" 37 | paramSpec ::= ID (":" typeSpec) 38 | varDeclItem ::= ID ("," ID)* ":" typeSpec 39 | varStmt ::= "export" varDeclItem ("," varDeclItem)* 40 | varStmt ::= "var" varDeclItem ("," varDeclItem)* 41 | typeVar ::= "`" ID ("[" typeSpec (("||"|"or") typeSpec)+ "]")? 42 | typeSpec ::= "bool" | "int" | "string" | "float" | "void" | 43 | "typespec" ("[" typeVar "]")? | 44 | "list" "[" typeSpec "]" | 45 | "dict" "[" typeSpec "," typeSpec "]" | 46 | "tuple" "[" typeSpec "," (typeSpec)+ "]" | 47 | "func" "(" (typeSpec ("," typeSpec)*)? ")" ("->" typeSpec)? | 48 | typeVar 49 | fnBody ::= "{" coreBodyItems* "}" 50 | # Note that literal matches before accessExpr, so a lparen at an exprStart 51 | # or in a unaryExpr will be treated as a tuple literal. 52 | exprStart ::= unaryExpr | notExpr | literal | accessExpr 53 | unaryExpr ::= ("+" | "-") (literal | accessExpr) 54 | notExpr ::= ("!" | "not") expression 55 | literal ::= NUM | STR | listLiteral | dictLiteral | typeSpec | TRUE | FALSE | OTHER 56 | accessExpr ::= (ID | "$" | parenExpr) (memberExpr | indexExpr | callActuals)* 57 | tupleLiteral ::= "(" expression ("," expression)*)+ ")" 58 | listLiteral ::= "[" (expression ("," expression)* )? "]" 59 | dictLiteral ::= "{" (expression ":" expression 60 | ("," expression ":" expression)*) "}" 61 | parenExpr ::= "(" expression ")" 62 | memberExpr ::= "." ID 63 | indexExpr ::= "[" expression "]" 64 | callActuals ::= "(" (expression ("," expression)* )? ")" 65 | expression ::= exprStart (orExpr*) 66 | orExpr ::= ("||" | "or") expression | andExpr 67 | andExpr ::= ("&&" | "and") andExprRHS | neExpr 68 | andExprRHS ::= exprStart (andExpr)* 69 | neExpr ::= "!=" neExprRHS | eqExpr 70 | neExprRHS ::= exprStart (neExpr)* 71 | eqExpr ::= "==" eqExprRHS | gteExpr 72 | eqExprRHS ::= exprStart (eqExpr)* 73 | gteExpr ::= ">=" gteExprRHS | lteExpr 74 | gteExprRHS ::= exprStart (gteExpr)* 75 | lteExpr ::= "<=" lteExprRHS | gtExpr 76 | lteExprRHS ::= exprStart (lteExpr)* 77 | gtExpr ::= ">" gtExprRHS | ltExpr 78 | gtExprRHS ::= exprStart (gtExpr)* 79 | ltExpr ::= "<" ltExprRHS | plusExpr 80 | ltExprRHS ::= exprStart (ltExpr)* 81 | plusExpr ::= "+" plusExprRHS | minusExpr 82 | plusExprRHS ::= exprStart (plusExpr)* 83 | minusExpr ::= "-" minusExprRHS | modExpr 84 | minusExprRHS ::= exprStart (minusExpr)* 85 | modExpr ::= "%" modExprRHS | mulExpr 86 | modExprRHS ::= exprStart (modExpr)* 87 | mulExpr ::= "*" mulExprRHS | divExpr 88 | mulExprRHS ::= exprStart (mulExpr)* 89 | divExpr ::= "/" divExprRHS | accessExpr 90 | divExprRHS ::= exprStart (divExpr)* 91 | # whileStmt ::= "while" expression "{" body "}" 92 | }% 93 | 94 | %{h Major Lexical elements }% 95 | Most of the lexical elements are inlined above, except for the following: 96 | %{c 97 | WS ::= (" " | "\\t")+ ; Whitespace 98 | NL ::= ("\\r\\n" | "\\n") ; Newline 99 | ID ::= IdStart (IdContinue)* ; As defined by unicode standard 100 | TRUE ::= "True" | "true" 101 | FALSE ::= "False" | "false" 102 | NUM ::= ("0".."9")+ 103 | ("." ("0".."9")+)? 104 | (("e"|"E") ("+"|"-")? ("0".."9")+)? 105 | STR ::= '"' ( ('\\\\' '"') | [^"\\n] )* '"' 106 | OTHER ::= "<<" .* ">>" 107 | LINECOMMENT ::= ("#" | "//") [^\n]* 108 | LONGCOMMENT ::= "/*" .* "*/" ; MINIMAL munch match. 109 | }% 110 | 111 | %{h Additional Notes }% 112 | %{# 113 | Early on, I removed the `while` operator, to ensure termination. I’ll probably add it back in as an optional item, or with an eval cost limit. 114 | Strings actually are parsed for escape sequences, and handle both `\uxxxx` and `\Uxxxxxxxx` formats. However, we do not currently look for hex escapes, since input config files are always expected to be valid Unicode (generally UTF-8). 115 | }% 116 | -------------------------------------------------------------------------------- /tests/basics/t1-hello.c4m: -------------------------------------------------------------------------------- 1 | x = 12485 2 | y = 400 3 | 4 | m = "foo" + "bar" 5 | 6 | echo(m) 7 | 8 | if (x > y) { echo("true") } 9 | -------------------------------------------------------------------------------- /tests/basics/t1-hello.kat: -------------------------------------------------------------------------------- 1 | foobar 2 | true 3 | -------------------------------------------------------------------------------- /tests/basics/t2-grabbag.c4m: -------------------------------------------------------------------------------- 1 | text := run("ls /bin/ps") 2 | 3 | # die := die + 1 4 | 5 | echo("Here's the output of ls:") 6 | echo(text) 7 | 8 | ctr := 0 9 | 10 | for i from 1 to 5 { 11 | echo(string(i)); ctr := ctr + 1 12 | } 13 | 14 | for i from 5 to 1 { 15 | echo(string(i)); ctr := ctr + 1 16 | } 17 | 18 | echo("I ran the counter " + string(ctr) + " times.") 19 | #echo(defaults.color) 20 | #echo(defaults.hair) 21 | #for i from 0 to len(defaults.faves) { 22 | # echo(defaults.faves[i]) 23 | #} 24 | -------------------------------------------------------------------------------- /tests/basics/t2-grabbag.kat: -------------------------------------------------------------------------------- 1 | Here's the output of ls: 2 | /bin/ps 3 | 4 | 1 5 | 2 6 | 3 7 | 4 8 | 4 9 | 3 10 | 2 11 | 1 12 | I ran the counter 8 times. 13 | -------------------------------------------------------------------------------- /tests/basics/t3-attr-basic.c4m: -------------------------------------------------------------------------------- 1 | defaults { 2 | color: "blue" 3 | hair: "wavy" 4 | faves: ["x", "y", "z"] 5 | } 6 | 7 | # die := die + 1 8 | 9 | echo(defaults.color) 10 | echo(defaults.hair) 11 | 12 | for i from 0 to len(defaults.faves) { 13 | echo(defaults.faves[i]) 14 | } 15 | -------------------------------------------------------------------------------- /tests/basics/t3-attr-basic.kat: -------------------------------------------------------------------------------- 1 | blue 2 | wavy 3 | x 4 | y 5 | z 6 | -------------------------------------------------------------------------------- /tests/basics/t4-enum.c4m: -------------------------------------------------------------------------------- 1 | enum A, B, C, D, E, F, G 2 | 3 | func test() { 4 | echo(format("The integer value for the enum val F is: {F}")) 5 | } 6 | 7 | test() 8 | -------------------------------------------------------------------------------- /tests/basics/t4-enum.kat: -------------------------------------------------------------------------------- 1 | The integer value for the enum val F is: 5 2 | -------------------------------------------------------------------------------- /tests/basics/t5-func.c4m: -------------------------------------------------------------------------------- 1 | func fact(f) { 2 | if (f <= 1) { 3 | result := f 4 | return 5 | } 6 | return fact(f - 1) * f 7 | } 8 | 9 | x := 0 10 | 11 | for i from 1 to 10 { 12 | x := fact(i) 13 | echo(format("Fact({i}) = {x}")) 14 | } 15 | -------------------------------------------------------------------------------- /tests/basics/t5-func.kat: -------------------------------------------------------------------------------- 1 | Fact(1) = 1 2 | Fact(2) = 2 3 | Fact(3) = 6 4 | Fact(4) = 24 5 | Fact(5) = 120 6 | Fact(6) = 720 7 | Fact(7) = 5040 8 | Fact(8) = 40320 9 | Fact(9) = 362880 10 | -------------------------------------------------------------------------------- /tests/basics/t6-dupe.c4m: -------------------------------------------------------------------------------- 1 | func fact(f) { 2 | if (f <= 1) { 3 | result := f 4 | return 5 | } 6 | return fact(f - 1) * f 7 | } 8 | 9 | x := 0 10 | 11 | for i from 1 to 10 { 12 | x := fact(i) 13 | echo(format("Fact({i}) = {x}")) 14 | } 15 | 16 | func doFact(f) { 17 | return fact(f) 18 | } 19 | 20 | func fact(f) { 21 | result := 1 22 | for i from 1 to f { 23 | result := result * i 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/basics/t6-dupe.kat: -------------------------------------------------------------------------------- 1 | error: con4m: /Users/viega/dev/con4m/tests/basics/t6-dupe.c4m: 20:1: Duplicate implementation of function 'fact' 2 | func fact(f) { 3 | ^ 4 | See con4m --help for help on usage. 5 | -------------------------------------------------------------------------------- /tests/basics/t7-fwref.c4m: -------------------------------------------------------------------------------- 1 | x := 0 2 | 3 | for i from 1 to 10 { 4 | x := doFact(i) 5 | echo(format("Fact({i}) = {x}")) 6 | } 7 | 8 | func fact(f) { 9 | result := 1 10 | for i from 1 to f { 11 | result := result * i 12 | } 13 | } 14 | 15 | func doFact(f) { 16 | return fact(f) 17 | } 18 | -------------------------------------------------------------------------------- /tests/basics/t7-fwref.kat: -------------------------------------------------------------------------------- 1 | Fact(1) = 1 2 | Fact(2) = 1 3 | Fact(3) = 2 4 | Fact(4) = 6 5 | Fact(5) = 24 6 | Fact(6) = 120 7 | Fact(7) = 720 8 | Fact(8) = 5040 9 | Fact(9) = 40320 10 | -------------------------------------------------------------------------------- /tests/basics/t8-attr-mem.c4m: -------------------------------------------------------------------------------- 1 | x.y.z = 10 2 | 3 | echo(string(x.y.z)) 4 | -------------------------------------------------------------------------------- /tests/basics/t8-attr-mem.kat: -------------------------------------------------------------------------------- 1 | 10 2 | -------------------------------------------------------------------------------- /tests/samibase.c4m: -------------------------------------------------------------------------------- 1 | ## This is the first configuration file loaded, and basically provides 2 | ## data and defaults. The bulk of it specifies the default metadata 3 | ## schema (key blocks). 4 | ## 5 | ## There's also metadata about the plugin modules and sinks we have 6 | ## available. 7 | ## 8 | ## :Author: John Viega (john@crashoverride.com) 9 | ## :Copyright: 2022, 2023, Crash Override, Inc. 10 | 11 | ## SAMI SCHEMA 12 | sami_version := "0.2.0" 13 | ascii_magic := "dadfedabbadabbed" 14 | 15 | key _MAGIC { 16 | required: true 17 | system: true 18 | squash: true 19 | type: "string" 20 | value: ascii_magic 21 | standard: true 22 | since: "0.1.0" 23 | output_order: 0 24 | in_ptr: true 25 | docstring: "Used to identify the start of JSON encoded output only" 26 | } 27 | 28 | key SAMI_ID { 29 | required: true 30 | codec: true 31 | squash: false 32 | type: "string" 33 | standard: true 34 | since: "0.1.0" 35 | output_order: 1 36 | in_ptr: true 37 | docstring: "Unique luid with a 48 bit time stamp and 80 bit random value" 38 | } 39 | 40 | key SAMI_VERSION { 41 | required: true 42 | system: false 43 | type: "string" 44 | value: sami_version 45 | standard: true 46 | since: "0.1.0" 47 | output_order: 2 48 | in_ptr: true 49 | docstring: "Spec version of inserted objects" 50 | } 51 | 52 | key SAMI_PTR { 53 | required: false 54 | type: "string" 55 | standard: true 56 | since: "0.1.0" 57 | output_order: 3 58 | in_ptr: true 59 | docstring: "When enabled, injects minimal metadata; this field then " + 60 | "gets a URL you supply to point to the rest of the data. " + 61 | "Items marked as 'true' in the 'Ptr?' column are added in " + 62 | "the PTR object" 63 | } 64 | 65 | key EARLIEST_VERSION { 66 | type: "string" 67 | since: "0.1.0" 68 | system: true 69 | skip: true 70 | value: sami_version 71 | output_order: 4 72 | standard: true 73 | docstring: "Reserved for future use" 74 | } 75 | 76 | key ARTIFACT_PATH { 77 | type: "string" 78 | since: "0.1.0" 79 | codec: true 80 | output_order: 5 81 | standard: true 82 | docstring: "Path of artifact on host where injection occured" 83 | } 84 | 85 | key INSERTION_HOSTINFO { 86 | type: "string" 87 | since: "0.1.0" 88 | output_order: 6 89 | standard: true 90 | docstring: "Host information at injection; by default, uname -a" 91 | } 92 | 93 | key ORIGIN_URI { 94 | type: "string" 95 | since: "0.1.0" 96 | output_order: 7 97 | standard: true 98 | docstring: "URI of origin for repo from which artifact was built" 99 | } 100 | 101 | key ARTIFACT_VERSION { 102 | type: "string" 103 | since: "0.1.0" 104 | output_order: 8 105 | standard: true 106 | docstring: "Reserved for future use" 107 | } 108 | 109 | key STORE_URI { 110 | type: "string" 111 | since: "0.1.0" 112 | output_order: 9 113 | standard: true 114 | docstring: "URI for the artifact storage location" 115 | } 116 | 117 | key BRANCH { 118 | type: "string" 119 | since: "0.1.0" 120 | standard: true 121 | output_order: 10 122 | docstring: "Version control branch the artifact was built from" 123 | } 124 | 125 | key HASH { 126 | type: "string" 127 | since: "0.1.0" 128 | codec: true 129 | standard: true 130 | output_order: 11 131 | docstring: "Hash file of artifact w/o SAMI in it, to ensure extracted " + 132 | "SAMIs are intact" 133 | } 134 | 135 | key HASH_FILES { 136 | type: "[string]" 137 | since: "0.1.0" 138 | codec: true 139 | standard: true 140 | output_order: 12 141 | docstring: "Files that constitute the artifact, and used in the hash, " + 142 | "if not just the ARTIFACT_PATH" 143 | } 144 | 145 | key COMPONENT_HASHES { 146 | type: "[string]" 147 | since: "0.1.0" 148 | codec: true 149 | standard: true 150 | output_order: 13 151 | docstring: "When there are multiple files hashed for a single " + 152 | "artifact, this contains SHA256 hashes of the individual " + 153 | "component hashes. The ordering will match the order of " + 154 | " the HASH_FILES field" 155 | } 156 | 157 | key COMMIT_ID { 158 | type: "string" 159 | since: "0.1.0" 160 | standard: true 161 | output_order: 14 162 | docstring: "The commit hash or id for the repository the artifact was " + 163 | "built from" 164 | } 165 | 166 | key CODE_OWNERS { 167 | type: "string" 168 | since: "0.1.0" 169 | standard: true 170 | output_order: 15 171 | docstring: "Code owners associated with the artifact" 172 | } 173 | 174 | key INJECTOR_ID { 175 | type: "string" 176 | since: "0.1.0" 177 | standard: true 178 | output_order: 16 179 | docstring: "SAMI ID of the sami binary that inserted metadata" 180 | } 181 | 182 | key INJECTOR_VERSION { 183 | type: "string" 184 | since: "0.1.0" 185 | standard: true 186 | system: true 187 | output_order: 17 188 | docstring: "Version of the SAMI binary used for injection" 189 | } 190 | 191 | key INJECTOR_PLATFORM { 192 | type: "string" 193 | since: "0.1.0" 194 | standard: true 195 | system: true 196 | output_order: 18 197 | docstring: "OS / system info at time of insertion" 198 | } 199 | 200 | key INJECTOR_COMMIT_ID { 201 | type: "string" 202 | since: "0.1.0" 203 | standard: true 204 | system: true 205 | output_order: 19 206 | docstring: "The commit hash or id from which the injector was built." 207 | } 208 | 209 | key BUILD_ID { 210 | type: "string" 211 | since: "0.1.0" 212 | standard: true 213 | output_order: 20 214 | docstring: "The ID of the CI/CD job" 215 | } 216 | 217 | key BUILD_URI { 218 | type: "string" 219 | since: "0.1.0" 220 | output_order: 21 221 | standard: true 222 | docstring: "URI to the CI/CD job" 223 | } 224 | 225 | key BUILD_API_URI { 226 | type: "string" 227 | since: "0.1.0" 228 | output_order: 22 229 | standard: true 230 | docstring: "URI to the CI/CD api if more information needs to be queried" 231 | } 232 | 233 | key BUILD_TRIGGER { 234 | type: "string" 235 | since: "0.1.0" 236 | output_order: 23 237 | standard: true 238 | docstring: "What event triggered CI/CD job" 239 | } 240 | 241 | key BUILD_CONTACT { 242 | type: "[string]" 243 | since: "0.1.0" 244 | output_order: 24 245 | standard: true 246 | docstring: "List of contacts which triggered CI/CD job" 247 | } 248 | 249 | key X_SAMI_CONFIG { 250 | system: true 251 | type: "string" 252 | since: "0.1.0" 253 | docstring: "Embedded configuration file" 254 | } 255 | 256 | key OLD_SAMI { 257 | type: "sami" 258 | since: "0.1.0" 259 | system: true 260 | standard: true 261 | output_order: high() - 6 262 | skip: true 263 | docstring: "Old SAMI info associated with an artifact, when a new " + 264 | "SAMI is inserted" 265 | } 266 | 267 | key EMBEDDED_SAMIS { 268 | type: "{string: sami}" 269 | since: "0.1.0" 270 | system: true 271 | standard: true 272 | output_order: high() - 5 273 | docstring: "Other artifacts contained in this artifact" 274 | } 275 | 276 | key SBOMS { 277 | type: "{string, string}" 278 | since: "0.1.0" 279 | standard: true 280 | output_order: high() - 4 281 | docstring: "SBOMs associated with this artifact" 282 | } 283 | 284 | key ERR_INFO { 285 | type: "[string]" 286 | standard: true 287 | since: "0.1.0" 288 | system: true 289 | output_order: high() - 3 290 | docstring: "Errors when inserting metadata" 291 | } 292 | 293 | key METADATA_HASH { 294 | type: "string" 295 | since: "0.1.0" 296 | system: true 297 | standard: true 298 | output_order: high() - 2 299 | in_ptr: true 300 | docstring: "A hash of all the preceding metadata, in a canonicalized " + 301 | "format. This includes user-defined keys." 302 | } 303 | 304 | key METADATA_ID { 305 | type: "string" 306 | since: "0.1.0" 307 | system: true 308 | standard: true 309 | output_order: high() - 1 310 | required: true 311 | in_ptr: true 312 | docstring: "A unique ID for this metadata, derived in part from the " + 313 | "hash of all the preceding metadata, in addition to the " + 314 | "current time." 315 | } 316 | 317 | key SIGN_PARAMS { 318 | type: "{string: string}" 319 | since: "0.1.0" 320 | system: true 321 | standard: true 322 | output_order: high() - 1 323 | in_ptr: true 324 | docstring: "Signing parameters for digital signatures." 325 | } 326 | 327 | key SIGNATURE { 328 | type: "string" 329 | since: "0.1.0" 330 | system: true 331 | standard: true 332 | output_order: high() 333 | in_ptr: true 334 | docstring: "Embedded digital signature for artifact" 335 | } 336 | 337 | # Doesn't do any keys other than the codec defaults, which are: 338 | # ARTIFACT_PATH, HASH, HASH_FILES, COMPONENT_HASHES. Note that 339 | # non-codecs cannot set these keys. 340 | 341 | plugin elf { 342 | codec: true 343 | keys: [] 344 | } 345 | 346 | plugin shebang { 347 | codec: true 348 | keys: [] 349 | } 350 | 351 | plugin container { 352 | codec: true 353 | keys: [] 354 | priority: 0 355 | enabled: true 356 | } 357 | 358 | 359 | # Probably should add file time of artifact, date of branch 360 | # and any tag associated. 361 | plugin vctl_git { 362 | keys: ["COMMIT_ID", "BRANCH", "ORIGIN_URI"] 363 | } 364 | 365 | plugin ci_github { 366 | keys: ["BUILD_ID", "BUILD_URI", "BUILD_API_URI", "BUILD_TRIGGER", 367 | "BUILD_CONTACT"] 368 | } 369 | 370 | plugin authors { 371 | keys: ["CODE_OWNERS"] 372 | } 373 | 374 | plugin github_codeowners { 375 | keys: ["CODE_OWNERS"] 376 | } 377 | 378 | plugin sbom_callback { 379 | keys: ["SBOMS"] 380 | } 381 | 382 | # This plugin is the only thing allowed to set these keys. It goes first 383 | # so that other plugins can use the data if need be. Priority should 384 | # thus set to low(int64), but con4m currently doesn't get this right, 385 | # low(int64) in con4m is -high(int64), when it should be one less. 386 | plugin system { 387 | keys: ["INJECTOR_ID", "INJECTOR_VERSION", "INJECTOR_PLATFORM", 388 | "INJECTOR_COMMIT_ID", "X_SAMI_CONFIG", "_MAGIC"] 389 | priority: 0 390 | } 391 | 392 | # These keys would be set by the `metsys` plugin, but need to go LAST 393 | # not first. The OLD_SAMI field needs to know what fields get written 394 | # before it can figure out what to write. 395 | # 396 | # 397 | # Also, this should be the place where signatures happen, though we 398 | # still have to work on that mechanism (a con4m callback probably). 399 | # 400 | # `metsys` is essentially meant to be "system end" in the sense of the 401 | # old algol days where an if statement's end delimeter was `fi` and a 402 | # case statement's end was `esac`. But also, this is where system 403 | # stuff goes that needs other metadata before it can write its own, so 404 | # I thought it was particularly appropriate. 405 | # 406 | # The priority field is set to high(int64). 407 | plugin metsys { 408 | keys: ["METADATA_ID", "OLD_SAMI", "SIGNATURE", "ERR_INFO"] 409 | priority: high() 410 | } 411 | 412 | # This plugin takes values from the conf file. By default, these 413 | # are of the lowest priority of anything that can conflict. 414 | # This will set SAMI_VERSION, EARLIEST_VERSION, SAMI_REF (if provided) 415 | # and _MAGIC. 416 | # 417 | # This plugin does attempt to apply substitutions to text strings. 418 | # If it fails, it just treats the value field as text. See the 419 | # plugin implementation for details on the substitutions. 420 | plugin conffile { 421 | keys: ["*"] 422 | priority: high() - 1 423 | } 424 | 425 | plugin custom_metadata { 426 | keys: ["*"] 427 | priority: 0 428 | } 429 | 430 | # Adding a sink requires a linked implementation. The existing sinks 431 | # are currently in the nimutils library, except for 'custom', which is 432 | # in output.nim. That one allows you to define your own sink in con4m 433 | # by supplying the callback outhook() 434 | # 435 | # If you do add more hardcoded sinks, please do make sure they get 436 | # locked in the lockBuiltinKeys() function in config.nim. 437 | 438 | sink stdout { 439 | docstring: "A sink that writes to stdout" 440 | } 441 | 442 | sink stderr { 443 | docstring: "A sink that writes to stderr" 444 | } 445 | 446 | sink file { 447 | needs_filename: true 448 | docstring: "A sink that writes a local file" 449 | } 450 | 451 | sink s3 { 452 | needs_secret: true 453 | needs_uid: true 454 | needs_uri: true 455 | uses_region: true 456 | uses_cacheid: true 457 | docstring: "A sink for S3 buckets" 458 | } 459 | 460 | sink post { 461 | needs_uri: true 462 | uses_headers: true 463 | docstring: "Generic HTTP/HTTPS post to a URL. Add custom headers " + 464 | "by providing an implementation to the callback " + 465 | "getPostHeaders(), which should return a dictionary where " + 466 | "all keys and values are strings." 467 | } 468 | 469 | sink custom { 470 | uses_secret: true 471 | uses_uid: true 472 | uses_filename: true 473 | uses_uri: true 474 | uses_region: true 475 | uses_cacheid: true 476 | uses_aux: true 477 | docstring: "Implement a custom sink via a con4m callback" 478 | } 479 | -------------------------------------------------------------------------------- /tests/spec/s1-basic.c4m: -------------------------------------------------------------------------------- 1 | root { 2 | require root { } 3 | allow object { } 4 | allow singleton { } 5 | allow exclusions { } 6 | user_def_ok: false 7 | } 8 | 9 | singleton root { 10 | allow field { } 11 | allow require { } 12 | allow allow { } 13 | allow exclusions { } 14 | field user_def_ok { 15 | type: "bool" 16 | require: true 17 | } 18 | user_def_ok: false 19 | } 20 | 21 | singleton exclusions { 22 | user_def_ok: true 23 | } 24 | 25 | object field { 26 | field type { 27 | type: "string" 28 | require: true 29 | } 30 | field default { 31 | type: "@x" 32 | require: true 33 | } 34 | field require { 35 | type: "bool" 36 | require: true 37 | } 38 | field write_lock { 39 | type: "bool" 40 | require: false 41 | } 42 | exclusions { 43 | default: "require" 44 | } 45 | user_def_ok: false 46 | } 47 | 48 | object singleton { 49 | allow field { } 50 | allow require { } 51 | allow allow { } 52 | allow exclusions { } 53 | field user_def_ok { 54 | type: "bool" 55 | require: true 56 | } 57 | user_def_ok: false 58 | } 59 | 60 | object object { 61 | allow field { } 62 | allow require { } 63 | allow allow { } 64 | allow exclusions { } 65 | field user_def_ok { 66 | type: "bool" 67 | require: true 68 | } 69 | user_def_ok: false 70 | } 71 | 72 | object allow { 73 | field write_lock { 74 | type: "bool" 75 | require: false # Write locking is not yet (re-)implemented. 76 | } 77 | user_def_ok: false 78 | } 79 | 80 | object require { 81 | field write_lock { 82 | type: "bool" 83 | require: false # Write locking is not yet (re-)implemented. 84 | } 85 | user_def_ok: false 86 | } 87 | -------------------------------------------------------------------------------- /tests/spec/s1-basic.kat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crashappsec/con4m/13e8f78982366246452789b85accbe3f9160ed12/tests/spec/s1-basic.kat -------------------------------------------------------------------------------- /tests/spec/s2-sami.c4m: -------------------------------------------------------------------------------- 1 | default_key_priority := 500 2 | object key { 3 | user_def_ok: false 4 | gen_fieldname: "keys" 5 | gen_typename: "KeySpec" 6 | 7 | field required { 8 | type: "bool" 9 | default: false 10 | # docstring: "..." 11 | } 12 | field system { 13 | type: "bool" 14 | default: false 15 | write_lock: true 16 | } 17 | field squash { 18 | type: "bool" 19 | default: false 20 | } 21 | field type { 22 | type: "typespec" 23 | require: true 24 | write_lock: true 25 | } 26 | field value { 27 | type: "=type" 28 | require: false 29 | } 30 | field standard { 31 | type: "bool" 32 | default: false 33 | write_lock: true 34 | } 35 | field since { 36 | type: "string" 37 | require: false 38 | write_lock: true 39 | } 40 | field skip { 41 | type: "bool" 42 | default: false 43 | } 44 | field in_ptr { 45 | type: "bool" 46 | default: false 47 | } 48 | field output_order { 49 | type: "int" 50 | default: default_key_priority 51 | range: (0, high()) 52 | write_lock: true 53 | stack_limit: 0 # Can't be provided when stacking, just on first run. 54 | } 55 | field docstring { 56 | type: "string" 57 | require: false 58 | } 59 | field codec { 60 | type: "bool" 61 | default: false 62 | write_lock: true 63 | } 64 | } 65 | 66 | object plugin { 67 | gen_fieldname: "plugins" 68 | gen_typename: "PluginSpec" 69 | user_def_ok: false 70 | 71 | field priority { 72 | type: "int" 73 | default: 50 74 | range: (0, high()) 75 | } 76 | field ignore { 77 | type: "[string]" 78 | default: [] 79 | } 80 | field codec { 81 | type: "bool" 82 | default: false 83 | write_lock: true 84 | } 85 | field keys { 86 | type: "[string]" 87 | require: true 88 | write_lock: true 89 | validator: "key_check" 90 | } 91 | field enabled { 92 | type: "bool" 93 | default: true 94 | } 95 | field overrides { 96 | type: "[string]" 97 | default: [] 98 | } 99 | field uses_fstream { 100 | type: "bool" 101 | default: true 102 | write_lock: true 103 | } 104 | field docstring { 105 | type: "string" 106 | require: false 107 | } 108 | } 109 | 110 | func key_check(keyname, value: [string]) { 111 | sects := sections("key") 112 | 113 | for i from 0 to len(value) { 114 | if value[i] == "*" { 115 | continue 116 | } 117 | if sects.contains(value[i]) { 118 | continue 119 | } else { 120 | return "'" + value[i] + "' is not a valid (specified) key." 121 | } 122 | } 123 | return "" 124 | } 125 | 126 | object sink { 127 | gen_fieldname: "sinks" 128 | gen_typename: "SinkSpec" 129 | user_def_ok: false 130 | 131 | field uses_secret { 132 | type: "bool" 133 | default: false 134 | write_lock: true 135 | } 136 | field uses_uid { 137 | type: "bool" 138 | default: false 139 | write_lock: true 140 | } 141 | field uses_filename { 142 | type: "bool" 143 | default: false 144 | write_lock: true 145 | } 146 | field uses_uri { 147 | type: "bool" 148 | default: false 149 | write_lock: true 150 | } 151 | field uses_region { 152 | type: "bool" 153 | default: false 154 | write_lock: true 155 | } 156 | field uses_headers { 157 | type: "bool" 158 | default: false 159 | write_lock: true 160 | } 161 | field uses_cacheid { 162 | type: "bool" 163 | default: false 164 | write_lock: true 165 | } 166 | field uses_aux { 167 | type: "bool" 168 | default: false 169 | write_lock: true 170 | } 171 | field needs_secret { 172 | type: "bool" 173 | default: false 174 | write_lock: true 175 | } 176 | field needs_uid { 177 | type: "bool" 178 | default: false 179 | write_lock: true 180 | } 181 | field needs_filename { 182 | type: "bool" 183 | default: false 184 | write_lock: true 185 | } 186 | field needs_uri { 187 | type: "bool" 188 | default: false 189 | write_lock: true 190 | } 191 | field needs_region { 192 | type: "bool" 193 | default: false 194 | write_lock: true 195 | } 196 | field needs_aux { 197 | type: "bool" 198 | default: false 199 | write_lock: true 200 | } 201 | field needs_headers { 202 | type: "bool" 203 | default: false 204 | write_lock: true 205 | } 206 | field needs_cacheid { 207 | type: "bool" 208 | default: false 209 | write_lock: true 210 | } 211 | field docstring { 212 | type: "string" 213 | require: false 214 | write_lock: true 215 | } 216 | } 217 | 218 | root { 219 | gen_typename: "ChalkConfig" 220 | user_def_ok: false 221 | 222 | allow key { } 223 | allow plugin { } 224 | allow sink { } 225 | 226 | field config_path { 227 | type: "[string]" 228 | default: [".", "~"] 229 | write_lock: true 230 | } 231 | field config_filename { 232 | type: "string" 233 | default: "sami.conf" 234 | write_lock: true 235 | } 236 | field default_command { 237 | type: "string" 238 | require: false 239 | choice: ["help", "insert", "extract", "delete", "help", 240 | "defaults", "confdump"] 241 | write_lock: true 242 | } 243 | field color { 244 | type: "bool" 245 | require: false 246 | } 247 | field log_level { 248 | type: "string" 249 | default: "warn" 250 | choice: ["verbose", "trace", "info", "warn", "error", "none"] 251 | } 252 | field sami_log_level { 253 | type: "string" 254 | default: "warn" 255 | choice: ["verbose", "trace", "info", "warn", "error", "none"] 256 | } 257 | field dry_run { 258 | type: "bool" 259 | default: false 260 | } 261 | field ignore_broken_conf { 262 | type: "bool" 263 | default: true 264 | } 265 | field con4m_pinpoint { 266 | type: "bool" 267 | default: true 268 | } 269 | field con4m_traces { 270 | type: "bool" 271 | default: false 272 | } 273 | field cache_fd_limit { 274 | type: "int" 275 | default: 50 276 | range: (0, high()) 277 | } 278 | field publish_audit { 279 | type: "bool" 280 | default: false 281 | write_lock: true 282 | } 283 | field artifact_search_path { 284 | type: "[string]" 285 | default: ["."] 286 | } 287 | field ignore_patterns { 288 | type: "[string]" 289 | default: [".*/**", "*.txt", "*.json"] 290 | } 291 | field recursive { 292 | type: "bool" 293 | default: true 294 | } 295 | field can_dump { 296 | type: "bool" 297 | default: true 298 | write_lock: true 299 | } 300 | field can_load { 301 | type: "bool" 302 | default: true 303 | write_lock: true 304 | } 305 | field container_image_id { 306 | type: "string" 307 | default: "" 308 | } 309 | field container_image_name { 310 | type: "string" 311 | default: "" 312 | } 313 | field allow_external_config { 314 | type: "bool" 315 | default: true 316 | write_lock: true 317 | } 318 | field publish_defaults { 319 | type: "bool" 320 | default: false 321 | write_lock: true 322 | } 323 | field publish_unmarked { 324 | type: "bool" 325 | default: true 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /tests/spec/s2-sami.kat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crashappsec/con4m/13e8f78982366246452789b85accbe3f9160ed12/tests/spec/s2-sami.kat -------------------------------------------------------------------------------- /tests/stack/1.kat: -------------------------------------------------------------------------------- 1 | hello, world! 2 | hello, world! 3 | hello, world! 4 | -------------------------------------------------------------------------------- /tests/stack/1/1.c4m: -------------------------------------------------------------------------------- 1 | $s := "hello, world!" 2 | -------------------------------------------------------------------------------- /tests/stack/1/2.c4m: -------------------------------------------------------------------------------- 1 | echo(s) 2 | -------------------------------------------------------------------------------- /tests/stack/1/3.c4m: -------------------------------------------------------------------------------- 1 | func f() { 2 | echo(s) 3 | } 4 | f() 5 | -------------------------------------------------------------------------------- /tests/stack/1/4.c4m: -------------------------------------------------------------------------------- 1 | f() 2 | -------------------------------------------------------------------------------- /tests/test.nim: -------------------------------------------------------------------------------- 1 | import unittest, nimutils, macros, os, strutils, osproc 2 | 3 | let passString = toAnsiCode(acBGreen) & "[PASSED]" & toAnsiCode(acReset) 4 | let failString = toAnsiCode(acBRed) & "[FAILED]" & toAnsiCode(acReset) 5 | var fails = 0 6 | 7 | macro runTests(dir: static[string], cmd: untyped): untyped = 8 | result = newStmtList() 9 | 10 | let dirpath = staticExec("pwd") & "/" & dir & "/" 11 | for filepath in staticListFiles(dirpath & "*.c4m"): 12 | let 13 | (dname, fname, ext) = filepath.splitFile() 14 | 15 | result.add quote do: 16 | try: 17 | let 18 | cmdline = `cmd` & `filepath` 19 | echo "running: ", cmdline 20 | let 21 | output = execCmdEx(cmdline).output.strip() 22 | kat = open(`dname` & "/" & `fname` & ".kat").readAll().strip() 23 | 24 | if output == kat: 25 | echo passString & " Test " & `fname` 26 | else: 27 | fails = fails + 1 28 | echo failString & " test " & `fname` 29 | echo "GOT:" 30 | echo output 31 | echo "EXPECTED: " 32 | echo kat 33 | except: 34 | fails = fails + 1 35 | echo "Exception raised: " 36 | echo getStackTrace() 37 | echo getCurrentExceptionMsg() 38 | 39 | 40 | macro runStackTests(dir: static[string], cmd: untyped): untyped = 41 | result = newStmtList() 42 | 43 | let toppath = staticExec("pwd") & "/" & dir & "/" 44 | for filepath in staticListFiles(toppath): 45 | if filepath.endsWith(".kat"): 46 | continue 47 | let 48 | (dname, fname, ext) = filepath.splitFile() 49 | katfile = toppath & dname & ".kat" 50 | dirpath = joinPath(toppath, filepath) 51 | stacklist = staticListFiles(dirpath) 52 | testname = "stack-" & dname 53 | var 54 | stackFiles: seq[string] = @[] 55 | 56 | for item in stackList: 57 | stackFiles.add(joinPath(dirpath, item)) 58 | 59 | let 60 | stackstr = stackfiles.join(" ") 61 | 62 | result.add quote do: 63 | try: 64 | let 65 | cmdline = `cmd` & `stackstr` 66 | echo "running: ", cmdline 67 | let 68 | output = execCmdEx(cmdline).output.strip() 69 | kat = open(`katfile`).readAll().strip() 70 | 71 | if output == kat: 72 | echo passString & " Test " & `testname` 73 | else: 74 | fails = fails + 1 75 | echo failString & " test " & `testname` 76 | echo "GOT:" 77 | echo output 78 | echo "EXPECTED: " 79 | echo kat 80 | except: 81 | fails = fails + 1 82 | echo "Exception raised: " 83 | echo getStackTrace() 84 | echo getCurrentExceptionMsg() 85 | 86 | runTests("basics"): 87 | "./con4m --no-color --none " 88 | 89 | runTests("spec"): 90 | "./con4m --no-color --none --c42 " 91 | 92 | runStackTests("stack"): 93 | "./con4m --no-color --none " 94 | 95 | check fails == 0 96 | --------------------------------------------------------------------------------