Skip to content

CFE-2986: Added getdir command to cf-net, copies directory and files#6049

Open
SimonThalvorsen wants to merge 1 commit intocfengine:masterfrom
SimonThalvorsen:CFE-2986
Open

CFE-2986: Added getdir command to cf-net, copies directory and files#6049
SimonThalvorsen wants to merge 1 commit intocfengine:masterfrom
SimonThalvorsen:CFE-2986

Conversation

@SimonThalvorsen
Copy link
Contributor

Ticket: CFE-2986
Changelog: Title

@SimonThalvorsen SimonThalvorsen requested a review from larsewi March 2, 2026 10:02
@larsewi larsewi changed the title Added getdir command to cf-net, copies directory and files CFE-2986: Added getdir command to cf-net, copies directory and files Mar 2, 2026
Copy link
Contributor

@larsewi larsewi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nicer if we extended cf-net get to get both files and directories, instead of having two separate sub-commands. Please investigate how rsync does this and try to do it in a similar way here.

@olehermanse
Copy link
Member

olehermanse commented Mar 4, 2026

It would be nicer if we extended cf-net get to get both files and directories, instead of having two separate sub-commands. Please investigate how rsync does this and try to do it in a similar way here.

@larsewi on the other hand, cf-net was written mainly as a protocol testing tool, and the commands mirror their protocol command names. If the GET protocol command in CFEngine network protocol can only GET a file, it might be a bit confusing that cf-net get is something very different.

There is also some advantage to not being too dynamic with this stuff. If you make a CLI that forces the user to specify if they are copying a file or a folder, you can prevent some annoying / confusing situations.

We could for example make a new subcommand, copy;

$ cf-net copy --file promises.cf
Copying file 1.2.3.4:/var/cfengine/masterfiles/promises.cf -> ./promises.cf
$ cf-net copy --file services/
Error: 'services/' does not look like a file, did you mean --directory?
$ cf-net copy --file services/.
Error: 'services/.' does not look like a file, did you mean --directory?
$ cf-net copy --directory services/
Copying directory 1.2.3.4:/var/cfengine/masterfiles/services/ -> ./services/
$ cf-net copy services/
Copying directory 1.2.3.4:/var/cfengine/masterfiles/services/ -> ./services/
$ cf-net copy services
Warning: Ambiguous if 'services' is a file or a directory - consider adding --file or --directory.
Copying directory 1.2.3.4:/var/cfengine/masterfiles/services/ -> ./services/

Copy link
Contributor

@larsewi larsewi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please break this up a little. Maybe a recursive solution would be more elegant given that it is a recursive problem.

cf-net/cf-net.c Outdated
filedata->print_stats = opts->print_stats;

filedata->ret = ProtocolStatGet(conn, filedata->remote_file,
filedata->local_file, 0644, filedata->print_stats);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you should use the remote file permissions (available from the ProtocolStat) instead?

cf-net/cf-net.c Outdated
char curr_dir[PATH_MAX];
snprintf(curr_dir, sizeof(curr_dir), "%s/%s", local_dir, curr);

GetFileData *filedata = calloc(1, sizeof(GetFileData));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not need to be heap allocated

cf-net/cf-net.c Outdated
continue;
}

snprintf(filedata->local_file, PATH_MAX, "%s", curr_dir);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check for truncation on all the calls to snprintf

cf-net/cf-net.c Outdated
Comment on lines +1034 to +1035
return invalid_command("getdir");
break;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unreachable break after return

cf-net/cf-net.c Outdated
if (local_dir != NULL)
{
Log(LOG_LEVEL_INFO,
"Warning: multiple occurences of -o in command, "\
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"Warning: multiple occurences of -o in command, "\
"Warning: multiple occurrences of -o in command, "\

char *local_dir = NULL;

int argc = 0;
while (args[argc] != NULL)

Check warning

Code scanning / CodeQL

Poorly documented large function Warning

Poorly documented function: fewer than 2% comments for a function of 115 lines.
@SimonThalvorsen SimonThalvorsen requested a review from larsewi March 9, 2026 14:42
Copy link
Contributor

@larsewi larsewi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be a recursion depth limit to prevent a potential malicious or misconfigured server with symlink loops (or deeply nested dirs) to cause stack overflow.

We should probably stick to a maximum path size for the same reasons above.

cf-net/cf-net.c Outdated
char local_full[PATH_MAX];
written = snprintf(local_full, sizeof(local_full), "%s/%s", local_path, item);
if (written < 0 || written >= sizeof(local_full)) {
Log(LOG_LEVEL_ERR, "Path too long for full local path: %s",local_full);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here you print the truncated path and not the intended full path. This could be a bit misleading.

if (written < 0 || written >= len) {
Log(LOG_LEVEL_ERR, "Path too long for local path: %s", temp);
free(local_dir);
return -1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

temp is leaked here

cf-net/cf-net.c Outdated
free(local_dir);
return -1;
}
int written = snprintf(temp, len, "%s/%s", local_dir, basename(remote_dir));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using StringFormat()

cf-net/cf-net.c Outdated
char *remote_dir = args[0];
if (specified_path)
{
size_t len = strlen(local_dir) + strlen(basename(remote_dir)) + 2; // / and '\0'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

basename() may modify it's argument

cf-net/cf-net.c Outdated
}

// Helper: Recursively process directory entries
static void process_dir_recursive(AgentConnection *conn,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function returns void. If all files fail to download, CFNetGetDir still returns 0 (success).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not see why this is an issue, failing to stat/get/mkdir will all log their respective errors. So you would rather it return -1 on any one error instead of continue?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you were to use it in a script, then it would be easier to check the exit value rather then parsing log messages. Does the other commands exit with 0 on failure?

}

// Helper: Get a single file with permissions
static bool CFNetGetWithPerms(AgentConnection *conn, const char *remote_path,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File permissions are preserved via ProtocolGet, but created directories don't get the remote directory's permissions applied.

cf-net/cf-net.c Outdated
SeqDestroy(items);
}

static int CFNetGetDir( CFNetOptions *opts, const char *hostname, char **args)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
static int CFNetGetDir( CFNetOptions *opts, const char *hostname, char **args)
static int CFNetGetDir(CFNetOptions *opts, const char *hostname, char **args)

cf-net/cf-net.c Outdated
return -1;
}
int written = snprintf(temp, len, "%s/%s", local_dir, basename(remote_dir));
if (written < 0 || written >= len) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (written < 0 || written >= len) {
if (written < 0 || written >= len)
{

cf-net/cf-net.c Outdated
assert(conn != NULL && remote_path != NULL && local_path != NULL);

struct stat perms;
if (!ProtocolStat(conn, remote_path, &perms)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try to be consistent with the braces. CONTRIBUTING.md says to put them on a new line. Look through the rest of the code, there are more cases like this.

Suggested change
if (!ProtocolStat(conn, remote_path, &perms)) {
if (!ProtocolStat(conn, remote_path, &perms))
{

cf-net/cf-net.c Outdated

if (has_output_path) {
written = snprintf(path, sizeof(path), "%s/%s/", local_base, subdir);
} else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use Allman style, not K&R style (} else {)

@SimonThalvorsen SimonThalvorsen force-pushed the CFE-2986 branch 2 times, most recently from d68100f to 047b068 Compare March 12, 2026 14:29
@SimonThalvorsen SimonThalvorsen requested a review from larsewi March 12, 2026 14:37
Copy link
Contributor

@larsewi larsewi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure to check return values of functions. It's OK to do best effort. I.e., we encounter an error but continue if it's safe. But in this case we need to cache the error so that we can later exit with failure. For some errors, like if we fail to create a directory, it does not make sense to try to fetch the files that should be in that directory afterwards.

cf-net/cf-net.c Outdated
static bool CFNetGetWithPerms(AgentConnection *conn, const char *remote_path,
const char *local_path, bool print_stats)
{
assert(conn != NULL && remote_path != NULL && local_path != NULL);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try this:

Suggested change
assert(conn != NULL && remote_path != NULL && local_path != NULL);
assert(conn != NULL);
assert(remote_path != NULL);
assert(local_path != NULL);

Maybe it will silence the CodeQL warning. It's also OK to simply dismiss it. However, by fixing it, the next time someone touches this code, the warning does not pop back up. I'm fine with either way.

cf-net/cf-net.c Outdated
Comment on lines +1048 to +1050
bool* created = NULL;
bool force = false;
MakeParentDirectoryPerms(path, force, created, perms);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I would just pass NULL here since you don't care if the directory was actually created or not. I get the idea that you want to improve readability with having "named" arguments. But in this case, it's more confusing than helpful.

Suggested change
bool* created = NULL;
bool force = false;
MakeParentDirectoryPerms(path, force, created, perms);
const bool force = false;
MakeParentDirectoryPerms(path, force, NULL, perms);

The intended use of this argument is:

    const bool force = false;
    bool created;
    MakeParentDirectoryPerms(path, force, &created, perms);
    if (created)
    {
        // The directory was created
    }
    else
    {
        // The directory already existed
    }

Where, created is set to true by the function itself if the directory was created.

The MakeParentDirectoryPerms function also returns an error. Maybe you should check this error message, and not try to fetch the files that should be inside the directory if you failed to create it.

cf-net/cf-net.c Outdated
Comment on lines +1055 to +1060
static int process_dir_recursive(AgentConnection *conn,
const char *remote_path,
const char *local_path,
bool has_output_path,
bool print_stats,
int limit)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent indents

Suggested change
static int process_dir_recursive(AgentConnection *conn,
const char *remote_path,
const char *local_path,
bool has_output_path,
bool print_stats,
int limit)
static int process_dir_recursive(AgentConnection *conn,
const char *remote_path,
const char *local_path,
bool has_output_path,
bool print_stats,
int limit)


// Helper: Create local directory path
static bool create_local_dir(const char *local_base, const char *subdir,
bool has_output_path, mode_t perms)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not obvious what the has_output_path variable does. I think this function deserves a doxygen comment.

}

int written;
Seq *items = ProtocolOpenDir(conn, remote_path);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure to free this sequence in all the return paths

cf-net/cf-net.c Outdated
bool print_stats,
int limit)
{
if (0 > limit - 1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This recursion limit test is confusing. Is not this the same as?

Suggested change
if (0 > limit - 1)
if (limit <= 0)

cf-net/cf-net.c Outdated
case 'l':
{
char *lim;
long val = strtol(optarg, &lim, 10);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to check for overflow. We have functions that do this in libntech. I would suggest using those.

cf-net/cf-net.c Outdated
args = &(args[optind]);
argc -= optind;
char *remote_dir = args[0];
char *base = basename(remote_dir);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

basename may modify its argument

cf-net/cf-net.c Outdated
assert(hostname != NULL);
assert(args != NULL);
char *local_dir = NULL;
int limit = 10;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default should probably be more than 10.

Suggested change
int limit = 10;
int limit = 32;

cf-net/cf-net.c Outdated
written = snprintf(path, sizeof(path), "%s/%s/%s/", cwd, local_base, subdir);
}

if (written < 0 || written >= sizeof(path))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static checkers may complain that you are comparing singed and unsigned. Consider casting to unsigned (safe since the negative case is handled first)

Suggested change
if (written < 0 || written >= sizeof(path))
if (written < 0 || (size_t) written >= sizeof(path))

Ticket: CFE-2986
Changelog: Title

Signed-off-by: Simon Halvorsen <simon.halvorsen@northern.tech>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants