Commit 6ff47d00 authored by jan.koester's avatar jan.koester
Browse files

args extended

parent c60dfb6d
Loading
Loading
Loading
Loading
+288 −0
Changes for commands/filesystem/copyutil.h: 288 added lines, 0 removed lines.
Original line number Diff line number Diff line
/*******************************************************************************
Copyright (c) 2026, Jan Koester jan.koester@gmx.net
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the <organization> nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/


#pragma once

// The copy engine behind cp and mv (for cross-device moves), following
// GNU cp: symlink dereferencing modes, recursive copies that recreate
// symlinks/fifos/devices, hardlink preservation, --preserve attributes,
// the same-file and copy-into-itself checks.

#include <iostream>
#include <string>
#include <map>
#include <vector>
#include <cstdlib>
#include <utility>
#include <cstring>
#include <cerrno>
#include <cstdio>

#include <unistd.h>
#include <fcntl.h>
#include <dirent.h>
#include <sys/stat.h>
#include <sys/sysmacros.h>

namespace fresh { namespace copy {

    enum class Deref { Never, CommandLine, Always };

    struct Options {
        const char *prog="cp";
        bool recursive=false;
        Deref deref=Deref::Always;
        bool force=false,interactive=false,noClobber=false,update=false;
        bool removeDest=false,verbose=false,oneFs=false;
        bool hardLink=false,symLink=false;
        bool keepMode=false,keepOwner=false,keepTimes=false,keepLinks=false;
        bool attributesOnly=false;
    };

    struct Copier {
        Options o;
        mode_t umaskBits=022;
        std::map<std::pair<dev_t,ino_t>,std::string> linkMap;   // for keepLinks
        dev_t rootDev=0;

        explicit Copier(const Options &opts):o(opts){
            umaskBits=umask(0);
            umask(umaskBits);
        }

        void err(const std::string &m){ std::cerr << o.prog << ": " << m << std::endl; }
        static std::string q(const std::string &s){ return "'"+s+"'"; }

        bool ask(const std::string &what){
            std::cerr << o.prog << ": " << what << "? ";
            std::string ans;
            char c;
            while(::read(0,&c,1)==1 && c!='\n') ans+=c;
            return !ans.empty() && (ans[0]=='y' || ans[0]=='Y');
        }

        void applyAttrs(const std::string &dst,const struct stat &st,bool isLink){
            if(o.keepOwner){
                if(lchown(dst.c_str(),st.st_uid,st.st_gid)!=0 && errno!=EPERM)
                    err("failed to preserve ownership for "+q(dst)+": "+strerror(errno));
                else if(errno==EPERM) lchown(dst.c_str(),(uid_t)-1,st.st_gid);
            }
            if(o.keepMode && !isLink){
                if(chmod(dst.c_str(),st.st_mode&07777)!=0)
                    err("preserving permissions for "+q(dst)+": "+strerror(errno));
            }
            if(o.keepTimes){
                struct timespec ts[2]={st.st_atim,st.st_mtim};
                utimensat(AT_FDCWD,dst.c_str(),ts,isLink ? AT_SYMLINK_NOFOLLOW : 0);
            }
        }

        bool copyRegular(const std::string &src,const std::string &dst,const struct stat &st,bool dstExists){
            int in=open(src.c_str(),O_RDONLY);
            if(in<0){ err("cannot open "+q(src)+" for reading: "+strerror(errno)); return false; }
            int out=-1;
            if(dstExists && !o.removeDest){
                out=open(dst.c_str(),O_WRONLY|O_TRUNC);
                if(out<0 && o.force){ unlink(dst.c_str()); dstExists=false; }
            }
            if(out<0){
                // new files: source mode, masked by the umask unless -p
                mode_t m=st.st_mode&0777;
                out=open(dst.c_str(),O_WRONLY|O_CREAT|O_TRUNC,o.keepMode ? 0600 : m);
                if(out<0){
                    err("cannot create regular file "+q(dst)+": "+strerror(errno));
                    close(in);
                    return false;
                }
            }
            bool ok=true;
            if(!o.attributesOnly){
                char buf[131072];
                for(;;){
                    ssize_t r=::read(in,buf,sizeof buf);
                    if(r<0){ if(errno==EINTR) continue; err("error reading "+q(src)+": "+strerror(errno)); ok=false; break; }
                    if(r==0) break;
                    ssize_t off=0;
                    while(off<r){
                        ssize_t w=::write(out,buf+off,(size_t)(r-off));
                        if(w<0){ if(errno==EINTR) continue; err("error writing "+q(dst)+": "+strerror(errno)); ok=false; break; }
                        off+=w;
                    }
                    if(!ok) break;
                }
            }
            close(in);
            if(close(out)!=0 && ok){ err("failed to close "+q(dst)+": "+strerror(errno)); ok=false; }
            if(ok) applyAttrs(dst,st,false);
            return ok;
        }

        // is `inner` the same as, or below, directory `outer`?
        static bool isInside(const std::string &outer,const std::string &inner){
            char a[4096],b[4096];
            if(!realpath(outer.c_str(),a)) return false;
            std::string in=inner;
            // dst may not exist yet: resolve its parent
            std::string rest;
            while(!realpath(in.c_str(),b)){
                size_t sl=in.find_last_of('/');
                if(sl==std::string::npos){ if(!getcwd(b,sizeof b)) return false; rest="/"+in+rest; break; }
                rest=in.substr(sl)+rest;
                in=sl==0 ? "/" : in.substr(0,sl);
                if(in=="/"){ strcpy(b,"/"); break; }
            }
            std::string full=std::string(b)+rest;
            std::string A=a;
            return full==A || (full.size()>A.size() && full.compare(0,A.size(),A)==0 && (A=="/" || full[A.size()]=='/'));
        }

        // copies src to exactly dst; top marks a command-line operand
        bool copy(const std::string &src,const std::string &dst,bool top){
            struct stat st;
            bool follow=o.deref==Deref::Always || (o.deref==Deref::CommandLine && top);
            if((follow ? stat(src.c_str(),&st) : lstat(src.c_str(),&st))!=0){
                err("cannot stat "+q(src)+": "+strerror(errno));
                return false;
            }
            if(top) rootDev=st.st_dev;
            struct stat ds;
            bool dstExists=lstat(dst.c_str(),&ds)==0;
            if(dstExists){
                struct stat dss=ds;
                if(S_ISLNK(ds.st_mode)) stat(dst.c_str(),&dss);
                if(dss.st_dev==st.st_dev && dss.st_ino==st.st_ino && !(S_ISLNK(st.st_mode) && S_ISLNK(ds.st_mode) && !o.symLink && !o.hardLink)){
                    err(q(src)+" and "+q(dst)+" are the same file");
                    return false;
                }
                if(S_ISDIR(ds.st_mode) && !S_ISDIR(st.st_mode)){
                    err("cannot overwrite directory "+q(dst)+" with non-directory "+q(src));
                    return false;
                }
                if(!S_ISDIR(ds.st_mode) && S_ISDIR(st.st_mode)){
                    err("cannot overwrite non-directory "+q(dst)+" with directory "+q(src));
                    return false;
                }
                if(!S_ISDIR(st.st_mode)){
                    if(o.noClobber) return true;
                    if(o.update){
                        if(ds.st_mtim.tv_sec>st.st_mtim.tv_sec || (ds.st_mtim.tv_sec==st.st_mtim.tv_sec && ds.st_mtim.tv_nsec>=st.st_mtim.tv_nsec))
                            return true;
                    }
                    if(o.interactive && !ask("overwrite "+q(dst))) return true;
                }
            }

            if(S_ISDIR(st.st_mode)){
                if(!o.recursive){
                    err("-r not specified; omitting directory "+q(src));
                    return false;
                }
                if(isInside(src,dst)){
                    err("cannot copy a directory, "+q(src)+", into itself, "+q(dst));
                    return false;
                }
                if(o.verbose && !dstExists) std::cout << q(src) << " -> " << q(dst) << std::endl;
                if(!dstExists){
                    if(mkdir(dst.c_str(),(st.st_mode&0777)|S_IRWXU)!=0){
                        err("cannot create directory "+q(dst)+": "+strerror(errno));
                        return false;
                    }
                }
                bool ok=true;
                if(!(o.oneFs && !top && st.st_dev!=rootDev)){
                    DIR *d=opendir(src.c_str());
                    if(!d){ err("cannot access "+q(src)+": "+strerror(errno)); return false; }
                    std::vector<std::string> names;
                    while(struct dirent *e=readdir(d)){
                        if(!strcmp(e->d_name,".") || !strcmp(e->d_name,"..")) continue;
                        names.push_back(e->d_name);
                    }
                    closedir(d);
                    for(auto &n: names){
                        std::string s2=src; if(s2.back()!='/') s2+='/';
                        std::string d2=dst; if(d2.back()!='/') d2+='/';
                        if(!copy(s2+n,d2+n,false)) ok=false;
                    }
                }
                if(o.keepMode || o.keepOwner || o.keepTimes) applyAttrs(dst,st,false);
                else if(!dstExists) chmod(dst.c_str(),(st.st_mode&0777)&~umaskBits);
                return ok;
            }

            if(o.verbose) std::cout << q(src) << " -> " << q(dst) << std::endl;

            if(o.hardLink || o.symLink){
                if(dstExists && (o.force || o.removeDest || o.interactive)) unlink(dst.c_str());
                int r=o.hardLink ? link(src.c_str(),dst.c_str()) : symlink(src.c_str(),dst.c_str());
                if(r!=0){
                    err(std::string("cannot create ")+(o.hardLink ? "hard" : "symbolic")+" link "+q(dst)+" to "+q(src)+": "+strerror(errno));
                    return false;
                }
                return true;
            }

            if(o.keepLinks && st.st_nlink>1 && !S_ISDIR(st.st_mode)){
                auto key=std::make_pair(st.st_dev,st.st_ino);
                auto it=linkMap.find(key);
                if(it!=linkMap.end()){
                    if(dstExists) unlink(dst.c_str());
                    if(link(it->second.c_str(),dst.c_str())==0) return true;
                    err("cannot create hard link "+q(dst)+" to "+q(it->second)+": "+strerror(errno));
                    return false;
                }
                linkMap[key]=dst;
            }

            if(S_ISLNK(st.st_mode)){
                char t[4096];
                ssize_t n=readlink(src.c_str(),t,sizeof t-1);
                if(n<0){ err("cannot read symbolic link "+q(src)+": "+strerror(errno)); return false; }
                t[n]=0;
                if(dstExists) unlink(dst.c_str());
                if(symlink(t,dst.c_str())!=0){
                    err("cannot create symbolic link "+q(dst)+": "+strerror(errno));
                    return false;
                }
                applyAttrs(dst,st,true);
                return true;
            }
            if(S_ISREG(st.st_mode) || !o.recursive) return copyRegular(src,dst,st,dstExists);
            // fifos, devices, sockets under -R: recreate the node
            if(dstExists) unlink(dst.c_str());
            int r=S_ISFIFO(st.st_mode) ? mkfifo(dst.c_str(),st.st_mode&07777)
                                       : mknod(dst.c_str(),st.st_mode,st.st_rdev);
            if(r!=0){
                err("cannot create special file "+q(dst)+": "+strerror(errno));
                return false;
            }
            applyAttrs(dst,st,false);
            return true;
        }
    };

}}
+111 −158

File changed.

Preview size limit exceeded, changes collapsed.

+148 −37
Changes for commands/filesystem/ln.cpp: 148 added lines, 37 removed lines.
Original line number Diff line number Diff line
@@ -27,63 +27,174 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#include <iostream>
#include <string>
#include <vector>
#include <cstring>
#include <cerrno>
#include <cstdlib>

#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>

#include "shell.h"
#include "../text/textutil.h"

namespace fresh { namespace cmd {

    // ln [-finrsvLPT] [-t DIR] TARGET... [LINK_NAME|DIR]
    class LnCommand : public ShellCommand {
    public:
        LnCommand():ShellCommand("ln"){}

        int runCommand(int argc,const char **argv){
            optind=0;
            opterr=0;

            bool symbolic=false,force=false;
            int opt;
            while((opt=getopt(argc,const_cast<char *const *>(argv),"sf"))!=-1){
                switch(opt){
                    case 's':
                        symbolic=true;
                        break;
                    case 'f':
                        force=true;
                        break;
                    default:
                        std::cerr << "ln: invalid option" << std::endl;
                        return 1;
        static std::string baseName(std::string p){
            while(p.size()>1 && p.back()=='/') p.pop_back();
            size_t sl=p.find_last_of('/');
            return sl==std::string::npos ? p : p.substr(sl+1);
        }
        static std::string dirName(std::string p){
            while(p.size()>1 && p.back()=='/') p.pop_back();
            size_t sl=p.find_last_of('/');
            if(sl==std::string::npos) return ".";
            return sl==0 ? "/" : p.substr(0,sl);
        }

            if(argc-optind!=2){
                std::cerr << "ln: usage: ln [-sf] TARGET LINKNAME" << std::endl;
                return 1;
        // canonical absolute path of p, with a possibly missing last part
        static std::string absPath(const std::string &p){
            char buf[4096];
            if(realpath(p.c_str(),buf)) return buf;
            std::string d=dirName(p),b=baseName(p);
            if(realpath(d.c_str(),buf)) return std::string(buf)+(std::string(buf)=="/" ? "" : "/")+b;
            return p;
        }
        // "ln -r": target relative to the link's directory
        static std::string relativeTo(const std::string &target,const std::string &linkName){
            std::string t=absPath(target),from=absPath(dirName(linkName));
            auto split=[](const std::string &s){
                std::vector<std::string> v; size_t p=0;
                while(p<s.size()){ size_t e=s.find('/',p); if(e==std::string::npos) e=s.size(); if(e>p) v.push_back(s.substr(p,e-p)); p=e+1; }
                return v;
            };
            auto a=split(t),b=split(from);
            size_t k=0;
            while(k<a.size() && k<b.size() && a[k]==b[k]) ++k;
            std::string r;
            for(size_t i=k;i<b.size();++i) r+="../";
            for(size_t i=k;i<a.size();++i){ r+=a[i]; if(i+1<a.size()) r+='/'; }
            if(r.empty()) r=".";
            if(r.size()>1 && r.back()=='/') r.pop_back();
            return r;
        }

            const char *target=argv[optind];
            const char *linkname=argv[optind+1];

            // "-f": remove an existing linkname first so it can be
            // replaced -- symlink()/link() both fail with EEXIST
            // otherwise. Failure here (most commonly ENOENT, nothing to
            // remove) is not itself an error; whatever actually stops the
            // real link below from being created is reported there.
            if(force)
                unlink(linkname);
        bool symbolic=false,force=false,interactive=false,relative=false,verbose=false,noDeref=false,logical=false;

            int rc=symbolic ? symlink(target,linkname) : link(target,linkname);
            if(rc!=0){
                std::cerr << "ln: " << "cannot link " << linkname << " to " << target
                           << ": " << strerror(errno) << std::endl;
                return 1;
        bool linkOne(const std::string &target,const std::string &name){
            std::string tgt=relative && symbolic ? relativeTo(target,name) : target;
            struct stat ns;
            if(lstat(name.c_str(),&ns)==0){
                if(S_ISDIR(ns.st_mode)){
                    std::cerr << "ln: " << name << ": cannot overwrite directory" << std::endl;
                    return false;
                }
                if(!symbolic){
                    struct stat ts;
                    if(stat(target.c_str(),&ts)==0 && ts.st_dev==ns.st_dev && ts.st_ino==ns.st_ino){
                        std::cerr << "ln: '" << target << "' and '" << name << "' are the same file" << std::endl;
                        return false;
                    }
                }
                if(interactive){
                    std::cerr << "ln: replace '" << name << "'? ";
                    std::string ans; char c;
                    while(::read(0,&c,1)==1 && c!='\n') ans+=c;
                    if(ans.empty() || (ans[0]!='y' && ans[0]!='Y')) return true;
                }else if(!force){
                    std::cerr << "ln: failed to create " << (symbolic ? "symbolic" : "hard") << " link '" << name << "': File exists" << std::endl;
                    return false;
                }
                if(unlink(name.c_str())!=0){
                    std::cerr << "ln: cannot remove '" << name << "': " << strerror(errno) << std::endl;
                    return false;
                }
            }
            int r=symbolic ? symlink(tgt.c_str(),name.c_str())
                           : linkat(AT_FDCWD,target.c_str(),AT_FDCWD,name.c_str(),logical ? AT_SYMLINK_FOLLOW : 0);
            if(r!=0){
                int e=errno;
                if(!symbolic && e==ENOENT){
                    struct stat ts;
                    if(lstat(target.c_str(),&ts)!=0){
                        std::cerr << "ln: failed to access '" << target << "': " << strerror(e) << std::endl;
                        return false;
                    }
                }
                std::cerr << "ln: failed to create " << (symbolic ? "symbolic" : "hard") << " link '" << name << "'"
                          << (symbolic ? "" : " => '"+target+"'") << ": " << strerror(e) << std::endl;
                return false;
            }
            if(verbose) std::cout << "'" << name << "' -> '" << tgt << "'" << std::endl;
            return true;
        }

            return 0;
        int runCommand(int argc,const char **argv){
            symbolic=force=interactive=relative=verbose=noDeref=logical=false;
            bool noTargetDir=false;
            std::string targetDir;
            text::Opts op=text::parseOpts(argc,argv,"tS",[&](char c,const std::string &v)->bool{
                switch(c){
                    case 's': symbolic=true; return true;
                    case 'f': force=true; interactive=false; return true;
                    case 'i': interactive=true; force=false; return true;
                    case 'r': relative=true; return true;
                    case 'v': verbose=true; return true;
                    case 'n': noDeref=true; return true;
                    case 'L': logical=true; return true;
                    case 'P': logical=false; return true;
                    case 'T': noTargetDir=true; return true;
                    case 't': targetDir=v; return true;
                    case 'b': case 'S': case 'd': case 'F': return true;
                    default: return text::badOpt("ln",c);
                }
            },[&](const std::string &n,const std::string &v,bool hasV)->int{
                if(n=="symbolic"){ symbolic=true; return 0; }
                if(n=="force"){ force=true; return 0; }
                if(n=="interactive"){ interactive=true; return 0; }
                if(n=="relative"){ relative=true; return 0; }
                if(n=="verbose"){ verbose=true; return 0; }
                if(n=="no-dereference"){ noDeref=true; return 0; }
                if(n=="logical"){ logical=true; return 0; }
                if(n=="physical"){ logical=false; return 0; }
                if(n=="no-target-directory"){ noTargetDir=true; return 0; }
                if(n=="target-directory"){ if(!hasV) return 1; targetDir=v; return 0; }
                if(n=="backup" || n=="suffix") return 0;
                return text::badLong("ln",n);
            });
            if(!op.ok) return 1;
            auto &ops=op.operands;
            if(ops.empty()){ std::cerr << "ln: missing file operand" << std::endl; return 1; }
            // is `p` a directory to put links into? (-n: a symlink to one is not)
            auto isDirTarget=[&](const std::string &p){
                struct stat st;
                if(noDeref){ if(lstat(p.c_str(),&st)!=0) return false; }
                else if(stat(p.c_str(),&st)!=0) return false;
                return S_ISDIR(st.st_mode);
            };
            if(!targetDir.empty()){
                if(!isDirTarget(targetDir)){ std::cerr << "ln: target '" << targetDir << "' is not a directory" << std::endl; return 1; }
                bool ok=true;
                for(auto &t: ops) if(!linkOne(t,targetDir+"/"+baseName(t))) ok=false;
                return ok ? 0 : 1;
            }
            if(ops.size()==1) return linkOne(ops[0],baseName(ops[0])) ? 0 : 1;
            std::string last=ops.back();
            if(noTargetDir){
                if(ops.size()>2){ std::cerr << "ln: extra operand '" << ops[2] << "'" << std::endl; return 1; }
                return linkOne(ops[0],last) ? 0 : 1;
            }
            if(ops.size()==2 && !isDirTarget(last)) return linkOne(ops[0],last) ? 0 : 1;
            if(!isDirTarget(last)){ std::cerr << "ln: target '" << last << "' is not a directory" << std::endl; return 1; }
            ops.pop_back();
            bool ok=true;
            for(auto &t: ops) if(!linkOne(t,last+(last.back()=='/' ? "" : "/")+baseName(t))) ok=false;
            return ok ? 0 : 1;
        }
    };

+128 −93

File changed.

Preview size limit exceeded, changes collapsed.

+88 −21

File changed.

Preview size limit exceeded, changes collapsed.

Loading