blob: ff7bff33e772cd9043a0ce8fb9680760ebca8c2c [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with this
* work for additional information regarding copyright ownership. The ASF
* licenses this file to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
#include "winutils.h"
#include <errno.h>
enum CHMOD_WHO
{
CHMOD_WHO_NONE = 0,
CHMOD_WHO_OTHER = 07,
CHMOD_WHO_GROUP = 070,
CHMOD_WHO_USER = 0700,
CHMOD_WHO_ALL = CHMOD_WHO_OTHER | CHMOD_WHO_GROUP | CHMOD_WHO_USER
};
enum CHMOD_OP
{
CHMOD_OP_INVALID,
CHMOD_OP_PLUS,
CHMOD_OP_MINUS,
CHMOD_OP_EQUAL,
};
enum CHMOD_PERM
{
CHMOD_PERM_NA = 00,
CHMOD_PERM_R = 01,
CHMOD_PERM_W = 02,
CHMOD_PERM_X = 04,
CHMOD_PERM_LX = 010,
};
/*
* We use the following struct to build a linked list of mode change actions.
* The mode is described by the following grammar:
* mode ::= clause [, clause ...]
* clause ::= [who ...] [action ...]
* action ::= op [perm ...] | op [ref]
* who ::= a | u | g | o
* op ::= + | - | =
* perm ::= r | w | x | X
* ref ::= u | g | o
*/
typedef struct _MODE_CHANGE_ACTION
{
USHORT who;
USHORT op;
USHORT perm;
USHORT ref;
struct _MODE_CHANGE_ACTION *next_action;
} MODE_CHANGE_ACTION, *PMODE_CHANGE_ACTION;
const MODE_CHANGE_ACTION INIT_MODE_CHANGE_ACTION = {
CHMOD_WHO_NONE, CHMOD_OP_INVALID, CHMOD_PERM_NA, CHMOD_WHO_NONE, NULL
};
static BOOL ParseOctalMode(LPCWSTR tsMask, INT *uMask);
static BOOL ParseMode(LPCWSTR modeString, PMODE_CHANGE_ACTION *actions);
static BOOL FreeActions(PMODE_CHANGE_ACTION actions);
static BOOL ParseCommandLineArguments(
__in int argc,
__in_ecount(argc) wchar_t *argv[],
__out BOOL *rec,
__out_opt INT *mask,
__out_opt PMODE_CHANGE_ACTION *actions,
__out LPCWSTR *path);
static BOOL ChangeFileModeByActions(__in LPCWSTR path,
MODE_CHANGE_ACTION const *actions);
static BOOL ChangeFileMode(__in LPCWSTR path, __in_opt INT mode,
__in_opt MODE_CHANGE_ACTION const *actions);
static BOOL ChangeFileModeRecursively(__in LPCWSTR path, __in_opt INT mode,
__in_opt MODE_CHANGE_ACTION const *actions);
//----------------------------------------------------------------------------
// Function: Chmod
//
// Description:
// The main method for chmod command
//
// Returns:
// 0: on success
//
// Notes:
//
int Chmod(__in int argc, __in_ecount(argc) wchar_t *argv[])
{
LPCWSTR pathName = NULL;
LPWSTR longPathName = NULL;
BOOL recursive = FALSE;
PMODE_CHANGE_ACTION actions = NULL;
INT unixAccessMask = 0;
DWORD dwRtnCode = 0;
int ret = EXIT_FAILURE;
// Parsing chmod arguments
//
if (!ParseCommandLineArguments(argc, argv,
&recursive, &unixAccessMask, &actions, &pathName))
{
fwprintf(stderr, L"Incorrect command line arguments.\n\n");
ChmodUsage(argv[0]);
return EXIT_FAILURE;
}
// Convert the path to the long path
//
dwRtnCode = ConvertToLongPath(pathName, &longPathName);
if (dwRtnCode != ERROR_SUCCESS)
{
ReportErrorCode(L"ConvertToLongPath", dwRtnCode);
goto ChmodEnd;
}
if (!recursive)
{
if (ChangeFileMode(longPathName, unixAccessMask, actions))
{
ret = EXIT_SUCCESS;
}
}
else
{
if (ChangeFileModeRecursively(longPathName, unixAccessMask, actions))
{
ret = EXIT_SUCCESS;
}
}
ChmodEnd:
FreeActions(actions);
LocalFree(longPathName);
return ret;
}
//----------------------------------------------------------------------------
// Function: ChangeFileMode
//
// Description:
// Wrapper function for change file mode. Choose either change by action or by
// access mask.
//
// Returns:
// TRUE: on success
// FALSE: otherwise
//
// Notes:
//
static BOOL ChangeFileMode(__in LPCWSTR path, __in_opt INT unixAccessMask,
__in_opt MODE_CHANGE_ACTION const *actions)
{
if (actions != NULL)
return ChangeFileModeByActions(path, actions);
else
{
DWORD dwRtnCode = ChangeFileModeByMask(path, unixAccessMask);
if (dwRtnCode != ERROR_SUCCESS)
{
ReportErrorCode(L"ChangeFileModeByMask", dwRtnCode);
return FALSE;
}
return TRUE;
}
}
//----------------------------------------------------------------------------
// Function: ChangeFileModeRecursively
//
// Description:
// Travel the directory recursively to change the permissions.
//
// Returns:
// TRUE: on success
// FALSE: otherwise
//
// Notes:
// The recursion works in the following way:
// - If the path is not a directory, change its mode and return.
// Symbolic links and junction points are not considered as directories.
// - Otherwise, call the method on all its children, then change its mode.
//
static BOOL ChangeFileModeRecursively(__in LPCWSTR path, __in_opt INT mode,
__in_opt MODE_CHANGE_ACTION const *actions)
{
BOOL isDir = FALSE;
BOOL isSymlink = FALSE;
LPWSTR dir = NULL;
size_t pathSize = 0;
size_t dirSize = 0;
HANDLE hFind = INVALID_HANDLE_VALUE;
WIN32_FIND_DATA ffd;
DWORD dwRtnCode = ERROR_SUCCESS;
BOOL ret = FALSE;
if ((dwRtnCode = DirectoryCheck(path, &isDir)) != ERROR_SUCCESS)
{
ReportErrorCode(L"IsDirectory", dwRtnCode);
return FALSE;
}
if ((dwRtnCode = SymbolicLinkCheck(path, &isSymlink)) != ERROR_SUCCESS)
{
ReportErrorCode(L"IsSymbolicLink", dwRtnCode);
return FALSE;
}
if (isSymlink || !isDir)
{
if (ChangeFileMode(path, mode, actions))
return TRUE;
else
return FALSE;
}
if (FAILED(StringCchLengthW(path, STRSAFE_MAX_CCH - 3, &pathSize)))
{
return FALSE;
}
dirSize = pathSize + 3;
dir = (LPWSTR)LocalAlloc(LPTR, dirSize * sizeof(WCHAR));
if (dir == NULL)
{
ReportErrorCode(L"LocalAlloc", GetLastError());
goto ChangeFileModeRecursivelyEnd;
}
if (FAILED(StringCchCopyW(dir, dirSize, path)) ||
FAILED(StringCchCatW(dir, dirSize, L"\\*")))
{
goto ChangeFileModeRecursivelyEnd;
}
hFind = FindFirstFile(dir, &ffd);
if (hFind == INVALID_HANDLE_VALUE)
{
ReportErrorCode(L"FindFirstFile", GetLastError());
goto ChangeFileModeRecursivelyEnd;
}
do
{
LPWSTR filename = NULL;
LPWSTR longFilename = NULL;
size_t filenameSize = 0;
if (wcscmp(ffd.cFileName, L".") == 0 ||
wcscmp(ffd.cFileName, L"..") == 0)
continue;
filenameSize = pathSize + wcslen(ffd.cFileName) + 2;
filename = (LPWSTR)LocalAlloc(LPTR, filenameSize * sizeof(WCHAR));
if (filename == NULL)
{
ReportErrorCode(L"LocalAlloc", GetLastError());
goto ChangeFileModeRecursivelyEnd;
}
if (FAILED(StringCchCopyW(filename, filenameSize, path)) ||
FAILED(StringCchCatW(filename, filenameSize, L"\\")) ||
FAILED(StringCchCatW(filename, filenameSize, ffd.cFileName)))
{
LocalFree(filename);
goto ChangeFileModeRecursivelyEnd;
}
// The child fileanme is not prepended with long path prefix.
// Convert the filename to long path format.
//
dwRtnCode = ConvertToLongPath(filename, &longFilename);
LocalFree(filename);
if (dwRtnCode != ERROR_SUCCESS)
{
ReportErrorCode(L"ConvertToLongPath", dwRtnCode);
LocalFree(longFilename);
goto ChangeFileModeRecursivelyEnd;
}
if(!ChangeFileModeRecursively(longFilename, mode, actions))
{
LocalFree(longFilename);
goto ChangeFileModeRecursivelyEnd;
}
LocalFree(longFilename);
} while (FindNextFileW(hFind, &ffd));
if (!ChangeFileMode(path, mode, actions))
{
goto ChangeFileModeRecursivelyEnd;
}
ret = TRUE;
ChangeFileModeRecursivelyEnd:
LocalFree(dir);
return ret;
}
//----------------------------------------------------------------------------
// Function: ParseCommandLineArguments
//
// Description:
// Parse command line arguments for chmod.
//
// Returns:
// TRUE: on success
// FALSE: otherwise
//
// Notes:
// 1. Recursive is only set on directories
// 2. 'actions' is NULL if the mode is octal
//
static BOOL ParseCommandLineArguments(
__in int argc,
__in_ecount(argc) wchar_t *argv[],
__out BOOL *rec,
__out_opt INT *mask,
__out_opt PMODE_CHANGE_ACTION *actions,
__out LPCWSTR *path)
{
LPCWSTR maskString;
BY_HANDLE_FILE_INFORMATION fileInfo;
DWORD dwRtnCode = ERROR_SUCCESS;
assert(path != NULL);
if (argc != 3 && argc != 4)
return FALSE;
*rec = FALSE;
if (argc == 4)
{
maskString = argv[2];
*path = argv[3];
if (wcscmp(argv[1], L"-R") == 0)
{
// Check if the given path name is a file or directory
// Only set recursive flag if the given path is a directory
//
dwRtnCode = GetFileInformationByName(*path, FALSE, &fileInfo);
if (dwRtnCode != ERROR_SUCCESS)
{
ReportErrorCode(L"GetFileInformationByName", dwRtnCode);
return FALSE;
}
if (IsDirFileInfo(&fileInfo))
{
*rec = TRUE;
}
}
else
return FALSE;
}
else
{
maskString = argv[1];
*path = argv[2];
}
if (ParseOctalMode(maskString, mask))
{
return TRUE;
}
else if (ParseMode(maskString, actions))
{
return TRUE;
}
return FALSE;
}
//----------------------------------------------------------------------------
// Function: FreeActions
//
// Description:
// Free a linked list of mode change actions given the head node.
//
// Returns:
// TRUE: on success
// FALSE: otherwise
//
// Notes:
// none
//
static BOOL FreeActions(PMODE_CHANGE_ACTION actions)
{
PMODE_CHANGE_ACTION curr = NULL;
PMODE_CHANGE_ACTION next = NULL;
// Nothing to free if NULL is passed in
//
if (actions == NULL)
{
return TRUE;
}
curr = actions;
while (curr != NULL)
{
next = curr->next_action;
LocalFree(curr);
curr = next;
}
actions = NULL;
return TRUE;
}
//----------------------------------------------------------------------------
// Function: ComputeNewMode
//
// Description:
// Compute a new mode based on the old mode and a mode change action.
//
// Returns:
// The newly computed mode
//
// Notes:
// Apply 'rwx' permission mask or reference permission mode according to the
// '+', '-', or '=' operator.
//
static INT ComputeNewMode(__in INT oldMode,
__in USHORT who, __in USHORT op,
__in USHORT perm, __in USHORT ref)
{
static const INT readMask = 0444;
static const INT writeMask = 0222;
static const INT exeMask = 0111;
INT mask = 0;
INT mode = 0;
// Operations are exclusive, and cannot be invalid
//
assert(op == CHMOD_OP_EQUAL || op == CHMOD_OP_PLUS || op == CHMOD_OP_MINUS);
// Nothing needs to be changed if there is not permission or reference
//
if(perm == CHMOD_PERM_NA && ref == CHMOD_WHO_NONE)
{
return oldMode;
}
// We should have only permissions or a reference target, not both.
//
assert((perm != CHMOD_PERM_NA && ref == CHMOD_WHO_NONE) ||
(perm == CHMOD_PERM_NA && ref != CHMOD_WHO_NONE));
if (perm != CHMOD_PERM_NA)
{
if ((perm & CHMOD_PERM_R) == CHMOD_PERM_R)
mask |= readMask;
if ((perm & CHMOD_PERM_W) == CHMOD_PERM_W)
mask |= writeMask;
if ((perm & CHMOD_PERM_X) == CHMOD_PERM_X)
mask |= exeMask;
if (((perm & CHMOD_PERM_LX) == CHMOD_PERM_LX))
{
// It applies execute permissions to directories regardless of their
// current permissions and applies execute permissions to a file which
// already has at least 1 execute permission bit already set (either user,
// group or other). It is only really useful when used with '+' and
// usually in combination with the -R option for giving group or other
// access to a big directory tree without setting execute permission on
// normal files (such as text files), which would normally happen if you
// just used "chmod -R a+rx .", whereas with 'X' you can do
// "chmod -R a+rX ." instead (Source: Wikipedia)
//
if ((oldMode & UX_DIRECTORY) == UX_DIRECTORY || (oldMode & exeMask))
mask |= exeMask;
}
}
else if (ref != CHMOD_WHO_NONE)
{
mask |= oldMode & ref;
switch(ref)
{
case CHMOD_WHO_GROUP:
mask |= mask >> 3;
mask |= mask << 3;
break;
case CHMOD_WHO_OTHER:
mask |= mask << 3;
mask |= mask << 6;
break;
case CHMOD_WHO_USER:
mask |= mask >> 3;
mask |= mask >> 6;
break;
default:
// Reference modes can only be U/G/O and are exclusive
assert(FALSE);
}
}
mask &= who;
if (op == CHMOD_OP_EQUAL)
{
mode = (oldMode & (~who)) | mask;
}
else if (op == CHMOD_OP_MINUS)
{
mode = oldMode & (~mask);
}
else if (op == CHMOD_OP_PLUS)
{
mode = oldMode | mask;
}
return mode;
}
//----------------------------------------------------------------------------
// Function: ConvertActionsToMask
//
// Description:
// Convert a linked list of mode change actions to the Unix permission mask
// given the head node.
//
// Returns:
// TRUE: on success
// FALSE: otherwise
//
// Notes:
// none
//
static BOOL ConvertActionsToMask(__in LPCWSTR path,
__in MODE_CHANGE_ACTION const *actions, __out PINT puMask)
{
MODE_CHANGE_ACTION const *curr = NULL;
DWORD dwErrorCode = ERROR_SUCCESS;
INT mode = 0;
dwErrorCode = FindFileOwnerAndPermission(path, FALSE, NULL, NULL, &mode);
if (dwErrorCode != ERROR_SUCCESS)
{
ReportErrorCode(L"FindFileOwnerAndPermission", dwErrorCode);
return FALSE;
}
*puMask = mode;
// Nothing to change if NULL is passed in
//
if (actions == NULL)
{
return TRUE;
}
for (curr = actions; curr != NULL; curr = curr->next_action)
{
mode = ComputeNewMode(mode, curr->who, curr->op, curr->perm, curr->ref);
}
*puMask = mode;
return TRUE;
}
//----------------------------------------------------------------------------
// Function: ChangeFileModeByActions
//
// Description:
// Change a file mode through a list of actions.
//
// Returns:
// TRUE: on success
// FALSE: otherwise
//
// Notes:
// none
//
static BOOL ChangeFileModeByActions(__in LPCWSTR path,
MODE_CHANGE_ACTION const *actions)
{
INT mask = 0;
if (ConvertActionsToMask(path, actions, &mask))
{
DWORD dwRtnCode = ChangeFileModeByMask(path, mask);
if (dwRtnCode != ERROR_SUCCESS)
{
ReportErrorCode(L"ChangeFileModeByMask", dwRtnCode);
return FALSE;
}
return TRUE;
}
else
return FALSE;
}
//----------------------------------------------------------------------------
// Function: ParseMode
//
// Description:
// Convert a mode string into a linked list of actions
//
// Returns:
// TRUE: on success
// FALSE: otherwise
//
// Notes:
// Take a state machine approach to parse the mode. Each mode change action
// will be a node in the output linked list. The state machine has five state,
// and each will only transit to the next; the end state can transit back to
// the first state, and thus form a circle. In each state, if we see a
// a character not belongs to the state, we will move to next state. WHO, PERM,
// and REF states are optional; OP and END states are required; and errors
// will only be reported at the latter two states.
//
static BOOL ParseMode(LPCWSTR modeString, PMODE_CHANGE_ACTION *pActions)
{
enum __PARSE_MODE_ACTION_STATE
{
PARSE_MODE_ACTION_WHO_STATE,
PARSE_MODE_ACTION_OP_STATE,
PARSE_MODE_ACTION_PERM_STATE,
PARSE_MODE_ACTION_REF_STATE,
PARSE_MODE_ACTION_END_STATE
} state = PARSE_MODE_ACTION_WHO_STATE;
MODE_CHANGE_ACTION action = INIT_MODE_CHANGE_ACTION;
PMODE_CHANGE_ACTION actionsEnd = NULL;
PMODE_CHANGE_ACTION actionsLast = NULL;
USHORT lastWho;
WCHAR c = 0;
size_t len = 0;
size_t i = 0;
assert(modeString != NULL && pActions != NULL);
if (FAILED(StringCchLengthW(modeString, STRSAFE_MAX_CCH, &len)))
{
return FALSE;
}
actionsEnd = *pActions;
while(i <= len)
{
c = modeString[i];
if (state == PARSE_MODE_ACTION_WHO_STATE)
{
switch (c)
{
case L'a':
action.who |= CHMOD_WHO_ALL;
i++;
break;
case L'u':
action.who |= CHMOD_WHO_USER;
i++;
break;
case L'g':
action.who |= CHMOD_WHO_GROUP;
i++;
break;
case L'o':
action.who |= CHMOD_WHO_OTHER;
i++;
break;
default:
state = PARSE_MODE_ACTION_OP_STATE;
} // WHO switch
}
else if (state == PARSE_MODE_ACTION_OP_STATE)
{
switch (c)
{
case L'+':
action.op = CHMOD_OP_PLUS;
break;
case L'-':
action.op = CHMOD_OP_MINUS;
break;
case L'=':
action.op = CHMOD_OP_EQUAL;
break;
default:
fwprintf(stderr, L"Invalid mode: '%s'\n", modeString);
FreeActions(*pActions);
return FALSE;
} // OP switch
i++;
state = PARSE_MODE_ACTION_PERM_STATE;
}
else if (state == PARSE_MODE_ACTION_PERM_STATE)
{
switch (c)
{
case L'r':
action.perm |= CHMOD_PERM_R;
i++;
break;
case L'w':
action.perm |= CHMOD_PERM_W;
i++;
break;
case L'x':
action.perm |= CHMOD_PERM_X;
i++;
break;
case L'X':
action.perm |= CHMOD_PERM_LX;
i++;
break;
default:
state = PARSE_MODE_ACTION_REF_STATE;
} // PERM switch
}
else if (state == PARSE_MODE_ACTION_REF_STATE)
{
switch (c)
{
case L'u':
action.ref = CHMOD_WHO_USER;
i++;
break;
case L'g':
action.ref = CHMOD_WHO_GROUP;
i++;
break;
case L'o':
action.ref = CHMOD_WHO_OTHER;
i++;
break;
default:
state = PARSE_MODE_ACTION_END_STATE;
} // REF switch
}
else if (state == PARSE_MODE_ACTION_END_STATE)
{
switch (c)
{
case L'\0':
__fallthrough;
case L',':
i++;
__fallthrough;
case L'+':
__fallthrough;
case L'-':
__fallthrough;
case L'=':
state = PARSE_MODE_ACTION_WHO_STATE;
// Append the current action to the end of the linked list
//
assert(actionsEnd == NULL);
// Allocate memory
actionsEnd = (PMODE_CHANGE_ACTION) LocalAlloc(LPTR,
sizeof(MODE_CHANGE_ACTION));
if (actionsEnd == NULL)
{
ReportErrorCode(L"LocalAlloc", GetLastError());
FreeActions(*pActions);
return FALSE;
}
if (action.who == CHMOD_WHO_NONE) action.who = CHMOD_WHO_ALL;
// Copy the action to the new node
*actionsEnd = action;
// Append to the last node in the linked list
if (actionsLast != NULL) actionsLast->next_action = actionsEnd;
// pActions should point to the head of the linked list
if (*pActions == NULL) *pActions = actionsEnd;
// Update the two pointers to point to the last node and the tail
actionsLast = actionsEnd;
actionsEnd = actionsLast->next_action;
// Reset action
//
lastWho = action.who;
action = INIT_MODE_CHANGE_ACTION;
if (c != L',')
{
action.who = lastWho;
}
break;
default:
fwprintf(stderr, L"Invalid mode: '%s'\n", modeString);
FreeActions(*pActions);
return FALSE;
} // END switch
}
} // while
return TRUE;
}
//----------------------------------------------------------------------------
// Function: ParseOctalMode
//
// Description:
// Convert the 3 or 4 digits Unix mask string into the binary representation
// of the Unix access mask, i.e. 9 bits each an indicator of the permission
// of 'rwxrwxrwx', i.e. user's, group's, and owner's read, write, and
// execute/search permissions.
//
// Returns:
// TRUE: on success
// FALSE: otherwise
//
// Notes:
// none
//
static BOOL ParseOctalMode(LPCWSTR tsMask, INT *uMask)
{
size_t tsMaskLen = 0;
DWORD i;
LONG l;
WCHAR *end;
if (uMask == NULL)
return FALSE;
if (FAILED(StringCchLengthW(tsMask, STRSAFE_MAX_CCH, &tsMaskLen)))
return FALSE;
if (tsMaskLen == 0 || tsMaskLen > 4)
{
return FALSE;
}
for (i = 0; i < tsMaskLen; i++)
{
if (!(tsMask[tsMaskLen - i - 1] >= L'0' &&
tsMask[tsMaskLen - i - 1] <= L'7'))
return FALSE;
}
errno = 0;
if (tsMaskLen == 4)
// Windows does not have any equivalent of setuid/setgid and sticky bit.
// So the first bit is omitted for the 4 digit octal mode case.
//
l = wcstol(tsMask + 1, &end, 8);
else
l = wcstol(tsMask, &end, 8);
if (errno || l > 0x0777 || l < 0 || *end != 0)
{
return FALSE;
}
*uMask = (INT) l;
return TRUE;
}
void ChmodUsage(LPCWSTR program)
{
fwprintf(stdout, L"\
Usage: %s [OPTION] OCTAL-MODE [FILE]\n\
or: %s [OPTION] MODE [FILE]\n\
Change the mode of the FILE to MODE.\n\
\n\
-R: change files and directories recursively\n\
\n\
Each MODE is of the form '[ugoa]*([-+=]([rwxX]*|[ugo]))+'.\n",
program, program);
}