/** * @file util.c * @brief Implements the util subsystem. */ #include #include #include #include #include #include #include #include #include #include #include #include #include /** * @brief Resolve @p path against @p root and write the absolute result to @p dest. * * The second half of akgl_path_relative: joins the two with a `/` and resolves * the join, so symlinks and `..` are folded out and the result is absolute. It * is the fallback, reached only once resolving @p path on its own has failed. * * `static`: it is reachable only through akgl_path_relative, which is the only * caller and the only sensible one. * * @param root Directory to resolve against, normally `dirname` of the file that * named @p path. Required. A trailing `/` is harmless; the join adds * one unconditionally and `realpath` collapses the double. * @param path Relative path to resolve. Required. An absolute @p path still gets * @p root pasted in front of it, which will not exist -- so callers * must not send absolute paths down this branch. * @param dest Receives the resolved absolute path. Required, and must already be * a claimed pool string. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_NULLPOINTER If @p root, @p path, or @p dest is `NULL`. * @throws AKERR_OUTOFBOUNDS If `root + path` is at least #AKGL_MAX_STRING_LENGTH * bytes. The message reports both the combined length and the limit. * @throws ENOENT If the joined path does not exist. Any other `errno` * `realpath(3)` raises -- EACCES, ELOOP, ENOTDIR -- propagates likewise. * @throws AKGL_ERR_HEAP If the string pool cannot supply the two scratch buffers. * * @note The AKERR_OUTOFBOUNDS check uses `FAIL_RETURN` from inside the `ATTEMPT` * block, which returns past `CLEANUP` -- so on that one path the two * scratch strings are never released. This is exactly the hazard AGENTS.md * warns about; it wants a `FAIL_BREAK`. */ static akerr_ErrorContext *path_relative_root(char *root, char *path, akgl_String *dest) { PREPARE_ERROR(errctx); akgl_String *pathbuf; akgl_String *strbuf; int rootlen; int pathlen; int count; FAIL_ZERO_RETURN(errctx, root, AKERR_NULLPOINTER, "NULL argument"); FAIL_ZERO_RETURN(errctx, path, AKERR_NULLPOINTER, "NULL argument"); FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL argument"); PASS(errctx, akgl_heap_next_string(&strbuf)); PASS(errctx, akgl_heap_next_string(&pathbuf)); ATTEMPT { // Is it relative to the root? rootlen = strlen(root); pathlen = strlen(path); if ( (rootlen + pathlen) >= AKGL_MAX_STRING_LENGTH ) { FAIL_BREAK(errctx, AKERR_OUTOFBOUNDS, "Total path length (%d) is greater than maximum akgl_String length (%d)", (rootlen + pathlen), AKGL_MAX_STRING_LENGTH); } CATCH(errctx, aksl_snprintf( &count, (char *)&pathbuf->data, sizeof(pathbuf->data), "%s/%s", root, path )); CATCH(errctx, aksl_realpath((char *)&pathbuf->data, (char *)&strbuf->data, sizeof(strbuf->data))); CATCH(errctx, akgl_string_copy(strbuf, dest, 0)); } CLEANUP { IGNORE(akgl_heap_release_string(strbuf)); IGNORE(akgl_heap_release_string(pathbuf)); } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } akerr_ErrorContext *akgl_path_relative(char *root, char *path, akgl_String *dest) { PREPARE_ERROR(errctx); akgl_String *strbuf; bool relative_to_root = false; FAIL_ZERO_RETURN(errctx, root, AKERR_NULLPOINTER, "NULL argument"); FAIL_ZERO_RETURN(errctx, path, AKERR_NULLPOINTER, "NULL argument"); FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL argument"); PASS(errctx, akgl_heap_next_string(&strbuf)); ATTEMPT { // Is path relative to our current working directory? CATCH(errctx, aksl_realpath(path, (char *)&strbuf->data, sizeof(strbuf->data))); // Yes it is. strbuf->data contains the absolute path. CATCH(errctx, akgl_string_copy(strbuf, dest, 0)); } CLEANUP { IGNORE(akgl_heap_release_string(strbuf)); } PROCESS(errctx) { } HANDLE(errctx, ENOENT) { // Path is not relative to our current working directory. Resolve it // against root instead -- but after FINISH, not from in here. Returning // from inside a HANDLE block skips the RELEASE_ERROR that FINISH ends // with, so the handled context is never given back to // AKERR_ARRAY_ERROR. That leaks one context per call, and the 129th // call takes the whole process down with "Unable to pull an error // context from the array!". Every map load resolves several paths this // way. relative_to_root = true; } FINISH(errctx, true); if ( relative_to_root == true ) { PASS(errctx, path_relative_root(root, path, dest)); } SUCCEED_RETURN(errctx); } akerr_ErrorContext *akgl_rectangle_points(akgl_RectanglePoints *dest, SDL_FRect *rect) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL akgl_RectanglePoints reference"); FAIL_ZERO_RETURN(errctx, rect, AKERR_NULLPOINTER, "NULL Rectangle reference"); dest->topleft.x = rect->x; dest->topleft.y = rect->y; dest->bottomleft.x = rect->x; dest->bottomleft.y = rect->y + rect->h; dest->topright.x = rect->x + rect->w; dest->topright.y = rect->y; dest->bottomright.x = rect->x + rect->w; dest->bottomright.y = rect->y + rect->h; SUCCEED_RETURN(errctx); } akerr_ErrorContext *akgl_collide_point_rectangle(akgl_Point *p, akgl_RectanglePoints *rp, bool *collide) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, p, AKERR_NULLPOINTER, "NULL Point reference"); FAIL_ZERO_RETURN(errctx, rp, AKERR_NULLPOINTER, "NULL akgl_RectanglePoints reference"); FAIL_ZERO_RETURN(errctx, collide, AKERR_NULLPOINTER, "NULL boolean reference"); if ( (p->x >= rp->topleft.x) && (p->y >= rp->topleft.y) && (p->x <= rp->bottomright.x) && (p->y <= rp->bottomright.y) ) { *collide = true; } else { *collide = false; } SUCCEED_RETURN(errctx); } akerr_ErrorContext *akgl_collide_rectangles(SDL_FRect *r1, SDL_FRect *r2, bool *collide) { akgl_RectanglePoints r1p; akgl_RectanglePoints r2p; PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, r1, AKERR_NULLPOINTER, "NULL rectangle reference"); FAIL_ZERO_RETURN(errctx, r2, AKERR_NULLPOINTER, "NULL rectangle reference"); FAIL_ZERO_RETURN(errctx, collide, AKERR_NULLPOINTER, "NULL collision flag reference"); ATTEMPT { CATCH(errctx, akgl_rectangle_points(&r1p, r1)); CATCH(errctx, akgl_rectangle_points(&r2p, r2)); // is the upper left corner of r1 contacting r2? CATCH(errctx, akgl_collide_point_rectangle(&r1p.topleft, &r2p, collide)); if ( *collide == true ) { break; } // is the upper left corner of r2 contacting r1? CATCH(errctx, akgl_collide_point_rectangle(&r2p.topleft, &r1p, collide)); if ( *collide == true ) { break; } // is the top right corner of r1 contacting r2? CATCH(errctx, akgl_collide_point_rectangle(&r1p.topright, &r2p, collide)); if ( *collide == true ) { break; } // is the top right corner of r2 contacting r1? CATCH(errctx, akgl_collide_point_rectangle(&r2p.topright, &r1p, collide)); if ( *collide == true ) { break; } // is the bottom left corner of r1 contacting r2? CATCH(errctx, akgl_collide_point_rectangle(&r1p.bottomleft, &r2p, collide)); if ( *collide == true ) { break; } // is the bottom left corner of r2 contacting r1? CATCH(errctx, akgl_collide_point_rectangle(&r2p.bottomleft, &r1p, collide)); if ( *collide == true ) { break; } // is the bottom right corner of r1 contacting r2? CATCH(errctx, akgl_collide_point_rectangle(&r1p.bottomright, &r2p, collide)); if ( *collide == true ) { break; } // is the bottom right corner of r2 contacting r1? CATCH(errctx, akgl_collide_point_rectangle(&r2p.bottomright, &r1p, collide)); if ( *collide == true ) { break; } } CLEANUP { } PROCESS(errctx) { } FINISH(errctx, true); // No trailing `*collide = false;` here. Each corner test writes the flag // itself, so the eighth one leaves it false when nothing hit; assigning // after FINISH would overwrite the hit that broke out of the block early. SUCCEED_RETURN(errctx); } akerr_ErrorContext *akgl_compare_sdl_surfaces(SDL_Surface *s1, SDL_Surface *s2) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, s1, AKERR_NULLPOINTER, "NULL Surface pointer"); FAIL_ZERO_RETURN(errctx, s2, AKERR_NULLPOINTER, "NULL Surface pointer"); // Geometry first. The memcmp reads s1->pitch * s1->h bytes out of *both*, // so a smaller s2 was read past its end rather than reported as a // mismatch -- and a differing pitch or format made the comparison // meaningless even when it did not run off. FAIL_NONZERO_RETURN( errctx, ((s1->w != s2->w) || (s1->h != s2->h)), AKERR_VALUE, "Comparison surfaces differ in size: %dx%d against %dx%d", s1->w, s1->h, s2->w, s2->h); FAIL_NONZERO_RETURN( errctx, (s1->pitch != s2->pitch), AKERR_VALUE, "Comparison surfaces differ in pitch: %d against %d", s1->pitch, s2->pitch); FAIL_NONZERO_RETURN( errctx, (s1->format != s2->format), AKERR_VALUE, "Comparison surfaces differ in pixel format: %s against %s", SDL_GetPixelFormatName(s1->format), SDL_GetPixelFormatName(s2->format)); FAIL_ZERO_RETURN(errctx, s1->pixels, AKERR_NULLPOINTER, "First surface has no pixels"); FAIL_ZERO_RETURN(errctx, s2->pixels, AKERR_NULLPOINTER, "Second surface has no pixels"); FAIL_NONZERO_RETURN(errctx, memcmp(s1->pixels, s2->pixels, (s1->pitch * s1->h)), AKERR_VALUE, "Comparison surfaces are not equal"); SUCCEED_RETURN(errctx); } akerr_ErrorContext *akgl_render_and_compare(SDL_Texture *t1, SDL_Texture *t2, int x, int y, int w, int h, char *writeout) { SDL_Surface *s1 = NULL; SDL_Surface *s2 = NULL; SDL_FRect src = {.x = x, .y = y, .w = w, .h = h}; SDL_FRect dest = {.x = x, .y = y, .w = w, .h = h}; SDL_Rect read = {.x = x, .y = y, .w = w, .h = h}; akgl_String *tmpstring = NULL; int count = 0; PREPARE_ERROR(errctx); ATTEMPT { FAIL_ZERO_BREAK(errctx, t1, AKERR_NULLPOINTER, "NULL texture"); FAIL_ZERO_BREAK(errctx, t2, AKERR_NULLPOINTER, "NULL texture"); CATCH(errctx, akgl_heap_next_string(&tmpstring)); SDL_RenderClear(akgl_renderer->sdl_renderer); CATCH(errctx, akgl_renderer->draw_texture(akgl_renderer, t1, &src, &dest, 0, NULL, SDL_FLIP_NONE)); s1 = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, &read); FAIL_ZERO_BREAK(errctx, s1, AKGL_ERR_SDL, "Failed to read pixels from renderer"); if ( writeout != NULL ) { CATCH(errctx, aksl_snprintf( &count, (char *)&tmpstring->data, sizeof(tmpstring->data), "%s%s", SDL_GetBasePath(), writeout)); FAIL_ZERO_BREAK( errctx, IMG_SavePNG(s1, (char *)&tmpstring->data), AKERR_IO, "Unable to save %s: %s", (char *)&tmpstring->data, SDL_GetError()); } SDL_RenderClear(akgl_renderer->sdl_renderer); CATCH(errctx, akgl_renderer->draw_texture(akgl_renderer, t2, &src, &dest, 0, NULL, SDL_FLIP_NONE)); s2 = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, &read); FAIL_ZERO_BREAK(errctx, s2, AKGL_ERR_SDL, "Failed to read pixels from renderer"); CATCH(errctx, akgl_compare_sdl_surfaces(s1, s2)); } CLEANUP { if ( s1 != NULL ) SDL_DestroySurface(s1); if ( s2 != NULL ) SDL_DestroySurface(s2); IGNORE(akgl_heap_release_string(tmpstring)); } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); }