| 1027 | } |
| 1028 | |
| 1029 | CBMLanguage cbm_language_from_shebang(const char *path) { |
| 1030 | if (!path) { |
| 1031 | return CBM_LANG_COUNT; |
| 1032 | } |
| 1033 | |
| 1034 | FILE *f = cbm_fopen(path, "rb"); |
| 1035 | if (!f) { |
| 1036 | return CBM_LANG_COUNT; /* fail closed on read error */ |
| 1037 | } |
| 1038 | |
| 1039 | /* Read only a bounded first line. */ |
| 1040 | char buf[CBM_SZ_256]; |
| 1041 | size_t n = fread(buf, SKIP_ONE, sizeof(buf) - SKIP_ONE, f); |
| 1042 | |
| 1043 | /* Fail closed on any read error rather than parsing a partial buffer. */ |
| 1044 | if (ferror(f)) { |
| 1045 | (void)fclose(f); |
| 1046 | return CBM_LANG_COUNT; |
| 1047 | } |
| 1048 | |
| 1049 | /* If the bounded buffer filled without containing a newline, the first |
| 1050 | * line may extend past our bound. Probe a single extra byte to tell an |
| 1051 | * exact EOF (the whole file is <= 255 bytes) from a truncated longer |
| 1052 | * line: any surviving byte -- including a newline just beyond the bound -- |
| 1053 | * means the first line was cut off, so fail closed. A probe read error |
| 1054 | * fails closed too. This keeps the read bounded (no unbounded line read |
| 1055 | * or allocation). */ |
| 1056 | bool have_newline = (memchr(buf, '\n', n) != NULL); |
| 1057 | if (!have_newline && n == sizeof(buf) - SKIP_ONE) { |
| 1058 | int probe = fgetc(f); |
| 1059 | if (probe != EOF || ferror(f)) { |
| 1060 | (void)fclose(f); |
| 1061 | return CBM_LANG_COUNT; |
| 1062 | } |
| 1063 | } |
| 1064 | (void)fclose(f); |
| 1065 | |
| 1066 | /* Must begin with "#!". */ |
| 1067 | if (n < PAIR_LEN || buf[0] != '#' || buf[1] != '!') { |
| 1068 | return CBM_LANG_COUNT; |
| 1069 | } |
| 1070 | |
| 1071 | /* Isolate the first line; reject an embedded NUL before the newline. */ |
| 1072 | size_t line_len = 0; |
| 1073 | while (line_len < n && buf[line_len] != '\n') { |
| 1074 | if (buf[line_len] == '\0') { |
| 1075 | return CBM_LANG_COUNT; /* embedded NUL — treat as binary */ |
| 1076 | } |
| 1077 | line_len++; |
| 1078 | } |
| 1079 | /* Trim a trailing CR so CRLF first lines parse. */ |
| 1080 | if (line_len > 0 && buf[line_len - SKIP_ONE] == '\r') { |
| 1081 | line_len--; |
| 1082 | } |
| 1083 | buf[line_len] = '\0'; |
| 1084 | |
| 1085 | /* First token after "#!" is the interpreter (or env). */ |
| 1086 | char *cursor = buf + PAIR_LEN; |
no test coverage detected