[Bitmap Project] File & Directory Validation

 

1. File & Directory Validation의 필요성

지난 포스팅을 통해 Command Line Argument로 옵션과 파일 이름등을 받을 수 있도록 설정했다. 하지만 코드를 눈여겨 봤다면 알겠지만 파일이름이 아니라 정확히는 실행파일의 경로이다.

relative path(상대경로)로 표현한 경우 파일 이름만 떡하니 있고 파일 경로가 드러나지 않았을 뿐이다. 상대 경로인 경우에도 bmputill와 같은 폴더에 있지 않은 경우, 폴더 이름들이 쭉 나오게 되고, absolute path(절대 경로)의 경우는 당연히 모든 경로가 적나라하게 드러난다.

실행파일과 다른 경로의 파일에 접근하기 시작할 여지를 주면서 여러가지 문제가 나타난다.

  1. 애초에 입력이 ‘경로(path)’가 아닌 엉뚱한 문자열인 경우
  2. 확장자가 엉뚱한 파일인 경우 (bmp, txt가 아닌 경우)
  3. 읽어야 하는 src파일이 존재하지 않는경우
  4. dst파일을 써야하는 경로가 존재하지 않는 경우

확장자 검사는 간단히 주어진 경로의 마지막 네 글자가 “.bmp” 또는 “.txt”인지 확인하는 것으로 하면 될 것 같다.

src파일의 경로를 검사할때는 읽어야하는 파일이 읽기 가능한지만 검사하면 되겠다. 경로의 존재 유무까지 파악할 필요가 없다. (존재하지 않으면 파일 자체가 유효하지 않으므로 걸러진다.)

그런데 dst파일은 존재하지 않아도 된다. 이미 존재하는 파일을 수정하는게 아니라 src파일을 변조하여 새로 만드는 것이었으니까. 하지만 경로자체는 존재해야한다. wb모드의 fopen이라고 해당경로의 모든 폴더까지 만들어주진 않는다. 그러니까 dst파일 경로로부터 파일이름을 제거하고, 남은 경로에 대한 유효성 검사부터 해주어야 하는 것이다.

1번 문제의 경우는 앞서 3, 4번 검사를 하면서 자연스럽게 걸러지므로 걱정하지 않아도 되겠다.

2. 관련 C 함수들

system함수로 shell에 명령어 전달하는 변태같은 방식을 써야하나, 아니면 혹시 관련 c 함수가 있나 검색하다가 오뇽님의 티스토리에서 관련 c 함수를 찾아냈다. 관련 MSDN문서를 좀 더 찾아보면서 이번에 필요한 함수들을 골라냈다.

  • int \_access(const char \*path, int mode);
    • path로 주어진 경로 또는 파일을 주어진 mode 권한으로 접근 가능한지 검사한다.
    • 접근 가능시 0, 불가능 시 -1을 반환한다.
    • <io.h>에 이 함수와 아래 표와 같이 mode들이 선언되어 있다. 짐작가능하겠지만, 비트필드로 각 권한을 검사하기 때문에 여러 개 옵션이 가능하다. (ex. 6은 Read and Write)
mode value (bin) checks file for
F_OK 0 (0b0000) Existence
X_OK 1 (0b0001) Executable
W_OK 2 (0b0010) Writealbe
R_OK 4 (0b0100) Readable
  • int \_mkdir(const char \*dirname);
    • dirname으로 경로와 폴더 이름이 전달된다. 해당 경로에 그 폴더를 생성한다.
    • 성공 시 0, 이미 존재하거나 유효하지 않은 이름인 경우 등 실패 시 -1을 반환한다.
    • <direct.h>에 선언 되어 있다.

그런데 이거 리눅스 시스템과 호환되지 않는다. 찾아보니 위 함수들은 윈도우 api란다. 하긴 파일 디렉토리 접근인데 그럴만 하다. 뭐 그럼 리눅스시스템에도 관련 함수가 있나 찾아봐야지.

역시 관련 내용이 담긴 블로그 1, 2를 찾았다. access는 사용용법이 동일한 듯 싶고, mkdir에는 파일권한을 설정하는 함수인자가 더 있다.

여기서 access를 사용하기위해 <unistd.h>, mkdir을 사용하기 위해 <sys/stat.h>, <sys/type.h>, <unistd.h>가 각각 필요함에 주목하여라.

3. 구현

3-1. 사용

지난 게시글에서 다시 가져와봤다.

int main(int argc, char* argv[]){
    if(argc!=4) goto INVALID;

    if(!isFileValid(argv[2],R_OK)) goto INVALID;
    if(!isFileValid(argv[3],W_OK)) goto INVALID;

    if(argv[1][0]!='-') goto INVALID;
    switch(argv[1][1]){
        case 'a': case 'A': toASCII(argv[2],argv[3]); break;
        case 'c': case 'C': contrast(argv[2],argv[3]); break;
        case 'g': case 'G': gray(argv[2],argv[3]); break;
        case 'i': case 'I': invert(argv[2],argv[3]); break;
        case 'm': case 'M': mirror(argv[2],argv[3]); break;
        default: goto INVALID; break;
    }

    return 0;
    
INVALID: ... (omitted)

EXAMPLE: ... (omitted)

}

argc==4를 확인한 후 isFileValid라는 함수를 통한 파일경로 유효성 검사를 추가하였다. 앞서 언급하였듯, src파일경로과 dst파일경로에서 검사해야할 포인트가 조금 다르기 때문에 구분을 위해 파일경로 뿐만아니라, mode도 받기로 했다. (access함수가 사용하는 매크로상수 재활용)

3-2. int isFileValid(char* file, int mode);

int isFileValid(char* file, int mode){
    if(mode&R_OK){
    	// 확장자가 bmp인지 검사
        return (access(file,R_OK)==0);
    }
    if(mode&W_OK){
    	// 확장자가 bmp 또는 txt인지 검사
        char path[??];
        // file로부터 path를 얻어내는 과정
        return (access(path,W_OK)==0);
    }
}

srcdst냐에 따라 조금 권한검사 방식이 달라지기 때문에 위와같이 구분을 했다. 보면 R_OK에서는 file을 검사하고, W_OK에서는 file로부터 path를 뽑아낸 후 path를 검사한다.

3-2-1. 확장자 검사

확장자 검사는 간단히 주어진 경로의 마지막 네 글자가 ".bmp" 또는 ".txt"인지 확인하는 것으로 하면 될 것 같다.

우선 확장자 검사는 위와 같이 말한적이 있다. 아래와 같이 해결할 수 있겠다.

strcmp(file+strlen(file)-4,".bmp");
strcmp(file+strlen(file)-4,".txt");

strlen함수 사용을 위해 <string.h>를 include해야함에 유의하자.

c의 함수들에서 string은 시작위치로부터 '\0', NULL까지만 인식한다는 점을 이용하면 복잡한 과정 없이 약간의 조작으로 해결가능하다. ".bmp"".txt" 모두 네 글자이므로 [strlen(file)-4]의 인덱스부터 읽기 시작하면 된다.

문제는 path인데… 파일이름을 인식해서 잘라낼 지점을 찾아내는 건 둘째치더라도,

c의 함수들에서 string은 시작위치로부터 '\0', NULL까지만 인식한다는 점

을 생각하면 파일이름과 폴더 경로 사이에 '\0'을 삽입해야 가능하다. path라는 새로운 배열공간을 만든 후 거기에 복사하는 것도 방법이겠다. 뭐 그건 나중에 생각하고…

3-2-2. Directory Separator: ‘/’ or ‘\’ ??

파일 이름은 어떻게 인식할까. Directory Separator '/'가 폴더 사이사이에 위치하므로 이걸 기준으로 인식하면 되겠다. 근데 이것도 꽤 빡센게… windows는 backslash '\'를 사용한다.

바다와 레거시

출처: twitter @malhomalho 16컷 만화 - 바다와 레거시

Unix / Linux 시스템 및 URL, 그리고 여타 다른 시스템과의 호환성 등 여러가지를 고려해, 현재는 윈도우즈의 cmd나 powershell은 둘 다 지원한다. 하지만, Slash '/'로 구분된 경로도 허용은 하지만, BackSlash '\'로 변환하는 처리 후, 내부적으로는 BackSlash '\'만을 사용하는 듯하다. 어찌되었건 두가지를 다 고려하여야겠다.

3-2-3. 경로 추출하기

int len = strlen(file);
for(--len;len>=0&&file[len]!='/'&&file[len]!='\\';--len);
if(len<0) return 1;
char path[len+1];
strncpy(path,file,len);
return (access(path,W_OK)==0 ? 1 : mkdir(path)==0 );

우선 어쨌든, file의 뒤부터 탐색하여 '/''\'를 찾게하였다. 거기서 잘라내면 파일이름과 경로가 구분되는 것이니까. 앞서,

… 파일이름과 폴더 경로 사이에 '\0'을 삽입해야 가능하다. path라는 새로운 배열공간을 만든 후 거기에 복사하는 것도 방법이겠다. …

이렇게 말했었다. 그래서 폴더 경로 영역만 strncpy로 따로 복사해서 path로 접근을 확인 후, 접근 불가면, 경로를 새로 만들도록 했다.

3-2-4. int mkdir(const char *dirname, mode_t mode);

어림도_없지

안 된다. 어림도 없지 예를 들어,

> ./bmputill -c sample.bmp /folder1/folder2/folder3/contrast.bmp

이렇게 입력을 했으며, 이때 최상위 폴더아래 folder1은 존재하지 않아 folder1, folder2, folder3 모두 만들어야 한다고 가정하자. (최상위 폴더가 접근 가능하다 가정하자.)

위 코드로 실행시 mkdir("/folder1/folder2/folder3");가 실행될 것이다. 하지만 folder1이 없기때문에 folder2에 접근도 되지 않는다. 그러면??? 하나하나 순차적으로 만들어 줘야 한다.

mkdir("/folder1",0777);
mkdir("/folder1/folder2",0777);
mkdir("/folder1/folder2/folder3",0777);

상대경로 접근시 현재 실행파일의 경로, 절대경로 접근시 최상위 폴더가 기준이 되기 때문에 상위경로들을 생략하면 엉뚱한 결과가 나온다. 우리가 의도한 것은 위의 순서로, 위 문자열 그대로 실행되어야한다.

3-3. int isPathValid(char* path,int len);

결국 경로 유효성검사하는 부분은 따로 빼내 함수로 만들기로 했다. (경로검사 하나씩 하면서 없는 폴더는 그때그때 만들어 주는 거다.)

이번엔 아까랑 반대다. 최상위 경로부터 하나하나 분리해내야한다. 다만 모든 폴더들을 분리해줘야해서 경로가 길어지면 배열을 여러 개 만들어야 한다. 그것도 개수가 정해져있지 않으므로 동적으로. 할 수야 있다만 더이상 매력적인 방법은 아닌 것 같다. strcpy는 시간적으로나 공간적으로나 낭비이다.

그럼 아까 말한 임시로 '\0' 삽입하기를 써야겠다.

int isPathValid(char* path,int len){
    for(int i=1;i<len;++i){
        if(path[i]=='/'||path[i]=='\\'){
            path[i]='\0';
            if(access(path,W_OK)==-1)
                if(mkdir(path,0777)==-1)
                    return 0;
            path[i]='/';
        }
    }
    return (access(path,W_OK)==0 ? 1 : (mkdir(path,0777)?0:1));
}

어떻게든 추출해낸 경로에서 처음부터 탐색하여 Directory Separator를 찾아내고, 찾아내면, 임시로 '\0'삽입 후, 유효성 검사와 (없을 시) mkdir을 실행한다. (실패시 종료) 다시 '/'를 넣어 복구후 반복을 계속 하다가, 반복문 종료후 마지막으로 종료 후 확인 한번 더 해주면 되겠다.

4. 최종 코드

이제 각 부분을 완성시켰으니 종합할 차례이다.

#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include "bitmap.h"

int isPathValid(char* path,int len){
    for(int i=1;i<len;++i){
        if(path[i]=='/'||path[i]=='\\'){
            path[i]='\0';
            if(access(path,W_OK)==-1)
                if(mkdir(path,0777)==-1)
                    return 0;
            path[i]='/';
        }
    }
    return (access(path,W_OK)==0 ? 1 : (mkdir(path,0777)?0:1));
}

int isFileValid(char* file, int mode){
    int len = strlen(file);
    if(mode&R_OK){
        if(strcmp(file+len-4,".bmp")) return 0;
        return (access(file,R_OK)==0);
    }
    if(mode&W_OK){
        if(strcmp(file+len-4,".bmp") && strcmp(file+len-4,".txt")) return 0;
        for(--len;len>=0&&file[len]!='/'&&file[len]!='\\';--len);
        if(len<0) return 1;
        file[len]='\0';
        int res = isPathValid(file,len);
        file[len]='/';
        return res;
    }
}

int main(int argc, char* argv[]){
    if(argc!=4) goto INVALID;

    if(!isFileValid(argv[2],R_OK)) goto INVALID;
    if(!isFileValid(argv[3],W_OK)) goto INVALID;

    if(argv[1][0]!='-') goto INVALID;
    switch(argv[1][1]){
        case 'a': case 'A': toASCII(argv[2],argv[3]); break;
        case 'c': case 'C': contrast(argv[2],argv[3]); break;
        case 'g': case 'G': gray(argv[2],argv[3]); break;
        case 'i': case 'I': invert(argv[2],argv[3]); break;
        case 'm': case 'M': mirror(argv[2],argv[3]); break;
        default: goto INVALID; break;
    }

    return 0;
    
INVALID: ... (omitted)

EXAMPLE: ... (omitted)

}

isFileValid를 조금 바꾸었다.

우선 strlen을 여러번 실행하게 되어서 변수로 만들어 한번만 실행하도록 하였다. strlen'\0'을 만날때까지 탐색하며 문자열의 길이를 측정하기때문에 불필요하게 여러번 호출하는 것은 바람직하지 않을 것 같다.

그 다음 path추출 방법도 새로운 공간에 복사하는 방법에서 임시로 '\0'을 넣어 해결하는 방식으로 바꾸었다. 앞서 언급했듯 복사 역시 (길이가 길어진다면) 오버헤드가 부담스러워진다. 어차피 isPathValid에서 NULL 삽입 방식으로 해결했기에 통일성을 주고자 같은 방법으로 통일하였다.

strncpy를 쓰지 않더라도 strlen<string.h>를 필요로 하는 점에 유의해야 한다.

This work is licensed under Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.
(The excerpted works are exceptionally subject to a licence from its source.) Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)