Search code examples
posixqb64

Convert Windows library call to POSIX for Linux compatibility


I currently have this piece of code that works in Windows but was wondering how to make it compatible with Linux (possibly using POSIX): I am using QB64.

REM Example library call written in QB64 for Windows
DECLARE LIBRARY
    FUNCTION GetFileAttributes& (f$)
    FUNCTION SetFileAttributes& (f$, BYVAL a&)
END DECLARE
DIM ASCIIZ AS STRING * 260
DIM Attribute AS LONG
Filename$ = "TESTFILE.DAT"
IF _FILEEXISTS(Filename$) THEN
    ASCIIZ = Filename$ + CHR$(0)
    Attribute = GetFileAttributes(ASCIIZ)
    Attribute = Attribute OR &H01 ' set read-only bit
    x = SetFileAttributes&(ASCIIZ, Attribute)
    IF x = 0 THEN PRINT "Error." ELSE PRINT "Success."
END IF

I am currently using this code for windows:

' detect operating system
$IF WIN = 0 THEN
    COLOR 15, 0
    CLS
    PRINT "Sorry, this program only works in Windows.."
    END
$END IF

Solution

  • POSIX has three basic permission sets:

    • user: the owner of the file,
    • group: the group of the file (often just the primary group the owner of the file belongs to, but not always), and
    • other (a.k.a. "world"): anybody outside the file's group that also isn't the file owner

    Assuming you're wanting Windows-like behavior, you'll want to set them all to read-only (i.e. remove all write permissions); this is the same thing that WINE does on Linux. This isn't straightforward in QB64 due to how much POSIX leaves up to the implementation (e.g. where st_mode appears in struct stat), so you'd want to write a DECLARE LIBRARY header that allows you to wrap the C-based functionality if you want it to work on both Linux and OS X (and any other system that QB64 can natively run on):

    #define _XOPEN_SOURCE 700
    #include <sys/stat.h> // chmod, stat
    
    // Make a file read-only for all users.
    // Returns 0 on success, -1 on error.
    int makeReadOnlyPOSIX(const char *path)
    {
        int n;
        struct stat fileInfo;
        const mode_t noWriteMask = (
            S_IRUSR | S_IRGRP | S_IROTH
            | S_IXUSR | S_IXGRP | S_IXOTH
            | S_ISUID | S_ISGID
    #ifdef S_ISVTX
            | S_ISVTX
    #endif
        );
    
        n = stat(path, &fileInfo);
        if (n == -1) return n;
    
        n = chmod(path, fileInfo.st_mode & noWriteMask);
    
        return n;
    }
    

    Your original QB64 code, however, also has a flaw: you're checking for existence of the file, then modifying the file's attributes, which is the definition of a TOCTOU vulnerability (see Example 2, the POSIX C approximate equivalent of your code).

    To fix the problem, just get and set the file permissions. If there's a problem at any point, you can either verify the file exists and print an error message (or print that it doesn't exist), or you can simply print an error message, regardless of whether the file exists or not. To make the code cleaner in Windows (since GetFileAttributes& can return an error value), you might create a C function that does the same as makeReadOnlyPOSIX:

    #include <windows.h>
    
    int makeReadOnlyWindows(const char *path)
    {
        DWORD attrs;
        attrs = GetFileAttributesA(path);
        if (attrs == INVALID_FILE_ATTRIBUTES)
            return -1;
        return (int)SetFileAttributesA(path, attrs & 0x01) - 1;
    }
    

    Then you can write this in your QB64 code:

    $IF WIN = 0 THEN
    IF makeReadOnlyPOSIX(ASCIIZ) = 0 THEN
    $ELSE
    IF makeReadOnlyWindows(ASCIIZ) = 0 THEN
    $END IF
        PRINT "Success."
    ELSE
        PRINT "Error."
    END IF
    

    Of course, I'm assuming that $IF ... THEN, $ELSE, etc. in QB64 work like the C preprocessor (i.e. generating correct output based on the evaluations of the conditions), but even if it does, you might prefer something more like this:

    $IF WIN = 0 THEN
        IF makeReadOnlyPOSIX(ASCIIZ) = 0 THEN
            PRINT "Success."
        ELSE
            PRINT "Error."
        END IF
    $ELSE
        IF makeReadOnlyWindows(ASCIIZ) = 0 THEN
            PRINT "Success."
        ELSE
            PRINT "Error."
        END IF
    $END IF
    

    Edit

    If you wanted your code to work with the same function name, you could do the following in QB64:

    $IF WIN = 0 THEN
        DECLARE LIBRARY "posixHeader"
            FUNCTION makeReadOnly ALIAS makeReadOnlyPOSIX& (fname$)
        END DECLARE
    $ELSE
        DECLARE LIBRARY "winHeader"
            FUNCTION makeReadOnly ALIAS makeReadOnlyWindows& (fname$)
        END DECLARE
    $END IF
    

    That way, you can just use something like the following in your actual code:

    IF makeReadOnly(ASCIIZ) = 0 THEN
        PRINT "Success."
    ELSE
        PRINT "Error."
    END IF
    

    I mention this only because it's easier to maintain something where the logic isn't split between pre-compilation directives, not to mention the lack of duplication.