github.com/pdfcpu/pdfcpu@v0.11.1/pkg/cli/list.go (about)

     1  /*
     2  Copyright 2023 The pdfcpu Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8  	http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Package cli provides pdfcpu command line processing.
    18  package cli
    19  
    20  import (
    21  	"crypto/x509"
    22  	"encoding/json"
    23  	"encoding/pem"
    24  	"fmt"
    25  	"io"
    26  	"math"
    27  	"os"
    28  	"path/filepath"
    29  	"sort"
    30  	"strconv"
    31  	"strings"
    32  	"time"
    33  
    34  	"github.com/hhrutter/pkcs7"
    35  	"github.com/pdfcpu/pdfcpu/pkg/api"
    36  	"github.com/pdfcpu/pdfcpu/pkg/log"
    37  	"github.com/pdfcpu/pdfcpu/pkg/pdfcpu"
    38  	"github.com/pdfcpu/pdfcpu/pkg/pdfcpu/form"
    39  	"github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model"
    40  	"github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types"
    41  	"github.com/pkg/errors"
    42  )
    43  
    44  func listAttachments(rs io.ReadSeeker, conf *model.Configuration, withDesc, sorted bool) ([]string, error) {
    45  	if rs == nil {
    46  		return nil, errors.New("pdfcpu: listAttachments: missing rs")
    47  	}
    48  
    49  	if conf == nil {
    50  		conf = model.NewDefaultConfiguration()
    51  	}
    52  	conf.Cmd = model.LISTATTACHMENTS
    53  
    54  	ctx, err := api.ReadAndValidate(rs, conf)
    55  	if err != nil {
    56  		return nil, err
    57  	}
    58  
    59  	aa, err := ctx.ListAttachments()
    60  	if err != nil {
    61  		return nil, err
    62  	}
    63  
    64  	var ss []string
    65  	for _, a := range aa {
    66  		s := a.FileName
    67  		if withDesc && a.Desc != "" {
    68  			s = fmt.Sprintf("%s (%s)", s, a.Desc)
    69  		}
    70  		ss = append(ss, s)
    71  	}
    72  	if sorted {
    73  		sort.Strings(ss)
    74  	}
    75  
    76  	return ss, nil
    77  }
    78  
    79  // ListAttachmentsFile returns a list of embedded file attachments of inFile with optional description.
    80  func ListAttachmentsFile(inFile string, conf *model.Configuration) ([]string, error) {
    81  	f, err := os.Open(inFile)
    82  	if err != nil {
    83  		return nil, err
    84  	}
    85  	defer f.Close()
    86  
    87  	return listAttachments(f, conf, true, true)
    88  }
    89  
    90  // ListAttachmentsCompactFile returns a list of embedded file attachments of inFile w/o optional description.
    91  func ListAttachmentsCompactFile(inFile string, conf *model.Configuration) ([]string, error) {
    92  	f, err := os.Open(inFile)
    93  	if err != nil {
    94  		return nil, err
    95  	}
    96  	defer f.Close()
    97  
    98  	return listAttachments(f, conf, false, false)
    99  }
   100  
   101  func listAnnotations(rs io.ReadSeeker, selectedPages []string, conf *model.Configuration) (int, []string, error) {
   102  	annots, err := api.Annotations(rs, selectedPages, conf)
   103  	if err != nil {
   104  		return 0, nil, err
   105  	}
   106  
   107  	return pdfcpu.ListAnnotations(annots)
   108  }
   109  
   110  // ListAnnotationsFile returns a list of page annotations of inFile.
   111  func ListAnnotationsFile(inFile string, selectedPages []string, conf *model.Configuration) (int, []string, error) {
   112  	f, err := os.Open(inFile)
   113  	if err != nil {
   114  		return 0, nil, err
   115  	}
   116  	defer f.Close()
   117  
   118  	return listAnnotations(f, selectedPages, conf)
   119  }
   120  
   121  func listBoxes(rs io.ReadSeeker, selectedPages []string, pb *model.PageBoundaries, conf *model.Configuration) ([]string, error) {
   122  	if rs == nil {
   123  		return nil, errors.New("pdfcpu: listBoxes: missing rs")
   124  	}
   125  
   126  	if conf == nil {
   127  		conf = model.NewDefaultConfiguration()
   128  	}
   129  	conf.Cmd = model.LISTBOXES
   130  
   131  	ctx, err := api.ReadAndValidate(rs, conf)
   132  	if err != nil {
   133  		return nil, err
   134  	}
   135  
   136  	pages, err := api.PagesForPageSelection(ctx.PageCount, selectedPages, true, true)
   137  	if err != nil {
   138  		return nil, err
   139  	}
   140  
   141  	return ctx.ListPageBoundaries(pages, pb)
   142  }
   143  
   144  // ListBoxesFile returns a list of page boundaries for selected pages of inFile.
   145  func ListBoxesFile(inFile string, selectedPages []string, pb *model.PageBoundaries, conf *model.Configuration) ([]string, error) {
   146  	f, err := os.Open(inFile)
   147  	if err != nil {
   148  		return nil, err
   149  	}
   150  	defer f.Close()
   151  
   152  	if pb == nil {
   153  		pb = &model.PageBoundaries{}
   154  		pb.SelectAll()
   155  	}
   156  	log.CLI.Printf("listing %s for %s\n", pb, inFile)
   157  
   158  	return listBoxes(f, selectedPages, pb, conf)
   159  }
   160  
   161  func listFormFields(rs io.ReadSeeker, conf *model.Configuration) ([]string, error) {
   162  	if conf == nil {
   163  		conf = model.NewDefaultConfiguration()
   164  	}
   165  	conf.Cmd = model.LISTFORMFIELDS
   166  
   167  	ctx, err := api.ReadAndValidate(rs, conf)
   168  	if err != nil {
   169  		return nil, err
   170  	}
   171  
   172  	return form.ListFormFields(ctx)
   173  }
   174  
   175  // ListFormFieldsFile returns a list of form field ids in inFile.
   176  func ListFormFieldsFile(inFiles []string, conf *model.Configuration) ([]string, error) {
   177  	log.SetCLILogger(nil)
   178  
   179  	ss := []string{}
   180  
   181  	for _, fn := range inFiles {
   182  
   183  		f, err := os.Open(fn)
   184  		if err != nil {
   185  			if len(inFiles) > 1 {
   186  				ss = append(ss, fmt.Sprintf("\ncan't open %s: %v", fn, err))
   187  				continue
   188  			}
   189  			return nil, err
   190  		}
   191  		defer f.Close()
   192  
   193  		output, err := listFormFields(f, conf)
   194  		if err != nil {
   195  			if len(inFiles) > 1 {
   196  				ss = append(ss, fmt.Sprintf("\n%s:\n%v", fn, err))
   197  				continue
   198  			}
   199  			return nil, err
   200  		}
   201  
   202  		ss = append(ss, "\n"+fn+":\n")
   203  		ss = append(ss, output...)
   204  	}
   205  
   206  	return ss, nil
   207  }
   208  
   209  func listImages(rs io.ReadSeeker, selectedPages []string, conf *model.Configuration) ([]string, error) {
   210  	if rs == nil {
   211  		return nil, errors.New("pdfcpu: listImages: Please provide rs")
   212  	}
   213  
   214  	if conf == nil {
   215  		conf = model.NewDefaultConfiguration()
   216  	}
   217  	conf.Cmd = model.LISTIMAGES
   218  
   219  	ctx, err := api.ReadValidateAndOptimize(rs, conf)
   220  	if err != nil {
   221  		return nil, err
   222  	}
   223  
   224  	pages, err := api.PagesForPageSelection(ctx.PageCount, selectedPages, true, true)
   225  	if err != nil {
   226  		return nil, err
   227  	}
   228  
   229  	return pdfcpu.ListImages(ctx, pages)
   230  }
   231  
   232  // ListImagesFile returns a formatted list of embedded images of inFile.
   233  func ListImagesFile(inFiles []string, selectedPages []string, conf *model.Configuration) ([]string, error) {
   234  	if len(selectedPages) == 0 {
   235  		log.CLI.Printf("pages: all\n")
   236  	}
   237  
   238  	log.SetCLILogger(nil)
   239  
   240  	ss := []string{}
   241  
   242  	for _, fn := range inFiles {
   243  		f, err := os.Open(fn)
   244  		if err != nil {
   245  			if len(inFiles) > 1 {
   246  				ss = append(ss, fmt.Sprintf("\ncan't open %s: %v", fn, err))
   247  				continue
   248  			}
   249  			return nil, err
   250  		}
   251  		defer f.Close()
   252  		output, err := listImages(f, selectedPages, conf)
   253  		if err != nil {
   254  			if len(inFiles) > 1 {
   255  				ss = append(ss, fmt.Sprintf("\n%s: %v", fn, err))
   256  				continue
   257  			}
   258  			return nil, err
   259  		}
   260  		ss = append(ss, "\n"+fn+":")
   261  		ss = append(ss, output...)
   262  	}
   263  
   264  	return ss, nil
   265  }
   266  
   267  // ListInfoFile returns formatted information about inFile.
   268  func ListInfoFile(inFile string, selectedPages []string, fonts bool, conf *model.Configuration) ([]string, error) {
   269  	f, err := os.Open(inFile)
   270  	if err != nil {
   271  		return nil, err
   272  	}
   273  	defer f.Close()
   274  
   275  	info, err := api.PDFInfo(f, inFile, selectedPages, fonts, conf)
   276  	if err != nil {
   277  		return nil, err
   278  	}
   279  
   280  	pages, err := api.PagesForPageSelection(info.PageCount, selectedPages, false, false)
   281  	if err != nil {
   282  		return nil, err
   283  	}
   284  
   285  	ss, err := pdfcpu.ListInfo(info, pages, fonts)
   286  	if err != nil {
   287  		return nil, err
   288  	}
   289  
   290  	return append([]string{inFile + ":"}, ss...), err
   291  }
   292  
   293  func jsonInfo(info *pdfcpu.PDFInfo, pages types.IntSet) (map[string]model.PageBoundaries, []types.Dim) {
   294  	if len(pages) > 0 {
   295  		pbs := map[string]model.PageBoundaries{}
   296  		for i, pb := range info.PageBoundaries {
   297  			if _, found := pages[i+1]; !found {
   298  				continue
   299  			}
   300  			d := pb.CropBox().Dimensions()
   301  			if pb.Rot%180 != 0 {
   302  				d.Width, d.Height = d.Height, d.Width
   303  			}
   304  			pb.Orientation = "portrait"
   305  			if d.Landscape() {
   306  				pb.Orientation = "landscape"
   307  			}
   308  			if pb.Media != nil {
   309  				pb.Media.Rect = pb.Media.Rect.ConvertToUnit(info.Unit)
   310  				pb.Media.Rect.LL.X = math.Round(pb.Media.Rect.LL.X*100) / 100
   311  				pb.Media.Rect.LL.Y = math.Round(pb.Media.Rect.LL.Y*100) / 100
   312  				pb.Media.Rect.UR.X = math.Round(pb.Media.Rect.UR.X*100) / 100
   313  				pb.Media.Rect.UR.Y = math.Round(pb.Media.Rect.UR.Y*100) / 100
   314  			}
   315  			if pb.Crop != nil {
   316  				pb.Crop.Rect = pb.Crop.Rect.ConvertToUnit(info.Unit)
   317  				pb.Crop.Rect.LL.X = math.Round(pb.Crop.Rect.LL.X*100) / 100
   318  				pb.Crop.Rect.LL.Y = math.Round(pb.Crop.Rect.LL.Y*100) / 100
   319  				pb.Crop.Rect.UR.X = math.Round(pb.Crop.Rect.UR.X*100) / 100
   320  				pb.Crop.Rect.UR.Y = math.Round(pb.Crop.Rect.UR.Y*100) / 100
   321  			}
   322  			if pb.Trim != nil {
   323  				pb.Trim.Rect = pb.Trim.Rect.ConvertToUnit(info.Unit)
   324  				pb.Trim.Rect.LL.X = math.Round(pb.Trim.Rect.LL.X*100) / 100
   325  				pb.Trim.Rect.LL.Y = math.Round(pb.Trim.Rect.LL.Y*100) / 100
   326  				pb.Trim.Rect.UR.X = math.Round(pb.Trim.Rect.UR.X*100) / 100
   327  				pb.Trim.Rect.UR.Y = math.Round(pb.Trim.Rect.UR.Y*100) / 100
   328  			}
   329  			if pb.Bleed != nil {
   330  				pb.Bleed.Rect = pb.Bleed.Rect.ConvertToUnit(info.Unit)
   331  				pb.Bleed.Rect.LL.X = math.Round(pb.Bleed.Rect.LL.X*100) / 100
   332  				pb.Bleed.Rect.LL.Y = math.Round(pb.Bleed.Rect.LL.Y*100) / 100
   333  				pb.Bleed.Rect.UR.X = math.Round(pb.Bleed.Rect.UR.X*100) / 100
   334  				pb.Bleed.Rect.UR.Y = math.Round(pb.Bleed.Rect.UR.Y*100) / 100
   335  			}
   336  			if pb.Art != nil {
   337  				pb.Art.Rect = pb.Art.Rect.ConvertToUnit(info.Unit)
   338  				pb.Art.Rect.LL.X = math.Round(pb.Art.Rect.LL.X*100) / 100
   339  				pb.Art.Rect.LL.Y = math.Round(pb.Art.Rect.LL.Y*100) / 100
   340  				pb.Art.Rect.UR.X = math.Round(pb.Art.Rect.UR.X*100) / 100
   341  				pb.Art.Rect.UR.Y = math.Round(pb.Art.Rect.UR.Y*100) / 100
   342  			}
   343  			pbs[strconv.Itoa(i+1)] = pb
   344  		}
   345  		return pbs, nil
   346  	}
   347  
   348  	var dims []types.Dim
   349  	for k, v := range info.PageDimensions {
   350  		if v {
   351  			dc := k.ConvertToUnit(info.Unit)
   352  			dc.Width = math.Round(dc.Width*100) / 100
   353  			dc.Height = math.Round(dc.Height*100) / 100
   354  			dims = append(dims, dc)
   355  		}
   356  	}
   357  	return nil, dims
   358  }
   359  
   360  func listInfoFilesJSON(inFiles []string, selectedPages []string, fonts bool, conf *model.Configuration) ([]string, error) {
   361  	var infos []*pdfcpu.PDFInfo
   362  
   363  	for _, fn := range inFiles {
   364  
   365  		f, err := os.Open(fn)
   366  		if err != nil {
   367  			return nil, err
   368  		}
   369  		defer f.Close()
   370  
   371  		info, err := api.PDFInfo(f, fn, selectedPages, fonts, conf)
   372  		if err != nil {
   373  			return nil, err
   374  		}
   375  
   376  		pages, err := api.PagesForPageSelection(info.PageCount, selectedPages, false, false)
   377  		if err != nil {
   378  			return nil, err
   379  		}
   380  
   381  		info.Boundaries, info.Dimensions = jsonInfo(info, pages)
   382  
   383  		infos = append(infos, info)
   384  	}
   385  
   386  	s := struct {
   387  		Header pdfcpu.Header     `json:"header"`
   388  		Infos  []*pdfcpu.PDFInfo `json:"infos"`
   389  	}{
   390  		Header: pdfcpu.Header{Version: "pdfcpu " + model.VersionStr, Creation: time.Now().Format("2006-01-02 15:04:05 MST")},
   391  		Infos:  infos,
   392  	}
   393  
   394  	bb, err := json.MarshalIndent(s, "", "\t")
   395  	if err != nil {
   396  		return nil, err
   397  	}
   398  
   399  	return []string{string(bb)}, nil
   400  }
   401  
   402  // ListInfoFiles returns formatted information about inFiles.
   403  func ListInfoFiles(inFiles []string, selectedPages []string, fonts, json bool, conf *model.Configuration) ([]string, error) {
   404  
   405  	if json {
   406  		return listInfoFilesJSON(inFiles, selectedPages, fonts, conf)
   407  	}
   408  
   409  	var ss []string
   410  
   411  	for i, fn := range inFiles {
   412  		if i > 0 {
   413  			ss = append(ss, "")
   414  		}
   415  		ssx, err := ListInfoFile(fn, selectedPages, fonts, conf)
   416  		if err != nil {
   417  			if len(inFiles) == 1 {
   418  				return nil, err
   419  			}
   420  			fmt.Fprintf(os.Stderr, "%s: %v\n", fn, err)
   421  		}
   422  		ss = append(ss, ssx...)
   423  	}
   424  
   425  	return ss, nil
   426  }
   427  
   428  // ListKeywordsFile returns the keyword list of inFile.
   429  func ListKeywordsFile(inFile string, conf *model.Configuration) ([]string, error) {
   430  	f, err := os.Open(inFile)
   431  	if err != nil {
   432  		return nil, err
   433  	}
   434  	defer f.Close()
   435  
   436  	return api.Keywords(f, conf)
   437  }
   438  
   439  func listPermissions(rs io.ReadSeeker, conf *model.Configuration) ([]string, error) {
   440  	if rs == nil {
   441  		return nil, errors.New("pdfcpu: listPermissions: missing rs")
   442  	}
   443  
   444  	if conf == nil {
   445  		conf = model.NewDefaultConfiguration()
   446  	}
   447  	conf.Cmd = model.LISTPERMISSIONS
   448  
   449  	ctx, err := api.ReadAndValidate(rs, conf)
   450  	if err != nil {
   451  		return nil, err
   452  	}
   453  
   454  	return pdfcpu.Permissions(ctx), nil
   455  }
   456  
   457  // ListPermissionsFile returns a list of user access permissions for inFile.
   458  func ListPermissionsFile(inFiles []string, conf *model.Configuration) ([]string, error) {
   459  	log.SetCLILogger(nil)
   460  
   461  	var ss []string
   462  
   463  	for i, fn := range inFiles {
   464  		if i > 0 {
   465  			ss = append(ss, "")
   466  		}
   467  		f, err := os.Open(fn)
   468  		if err != nil {
   469  			return nil, err
   470  		}
   471  		defer func() {
   472  			f.Close()
   473  		}()
   474  		ssx, err := listPermissions(f, conf)
   475  		if err != nil {
   476  			if len(inFiles) == 1 {
   477  				return nil, err
   478  			}
   479  			fmt.Fprintf(os.Stderr, "%s: %v\n", fn, err)
   480  		}
   481  		ss = append(ss, fn+":")
   482  		ss = append(ss, ssx...)
   483  	}
   484  
   485  	return ss, nil
   486  }
   487  
   488  func listProperties(rs io.ReadSeeker, conf *model.Configuration) ([]string, error) {
   489  	if rs == nil {
   490  		return nil, errors.New("pdfcpu: listProperties: missing rs")
   491  	}
   492  
   493  	if conf == nil {
   494  		conf = model.NewDefaultConfiguration()
   495  	} else {
   496  		conf.ValidationMode = model.ValidationRelaxed
   497  	}
   498  	conf.Cmd = model.LISTPROPERTIES
   499  
   500  	ctx, err := api.ReadAndValidate(rs, conf)
   501  	if err != nil {
   502  		return nil, err
   503  	}
   504  
   505  	return pdfcpu.PropertiesList(ctx)
   506  }
   507  
   508  // ListPropertiesFile returns the property list of inFile.
   509  func ListPropertiesFile(inFile string, conf *model.Configuration) ([]string, error) {
   510  	f, err := os.Open(inFile)
   511  	if err != nil {
   512  		return nil, err
   513  	}
   514  	defer f.Close()
   515  
   516  	return listProperties(f, conf)
   517  }
   518  
   519  func listBookmarks(rs io.ReadSeeker, conf *model.Configuration) ([]string, error) {
   520  	if rs == nil {
   521  		return nil, errors.New("pdfcpu: listBookmarks: missing rs")
   522  	}
   523  
   524  	if conf == nil {
   525  		conf = model.NewDefaultConfiguration()
   526  	} else {
   527  		conf.ValidationMode = model.ValidationRelaxed
   528  	}
   529  	conf.Cmd = model.LISTBOOKMARKS
   530  
   531  	ctx, err := api.ReadAndValidate(rs, conf)
   532  	if err != nil {
   533  		return nil, err
   534  	}
   535  
   536  	return pdfcpu.BookmarkList(ctx)
   537  }
   538  
   539  // ListBookmarksFile returns the bookmarks of inFile.
   540  func ListBookmarksFile(inFile string, conf *model.Configuration) ([]string, error) {
   541  	f, err := os.Open(inFile)
   542  	if err != nil {
   543  		return nil, err
   544  	}
   545  	defer f.Close()
   546  
   547  	return listBookmarks(f, conf)
   548  }
   549  
   550  func listPEM(fName string) (int, error) {
   551  	bb, err := os.ReadFile(fName)
   552  	if err != nil {
   553  		fmt.Printf("%v\n", err)
   554  		return 0, err
   555  	}
   556  
   557  	if len(bb) == 0 {
   558  		//return 0, errors.Errorf("%s is empty\n", filepath.Base(fName))
   559  		return 0, errors.New("is empty\n")
   560  	}
   561  
   562  	ss := []string{}
   563  	for len(bb) > 0 {
   564  		var block *pem.Block
   565  		block, bb = pem.Decode(bb)
   566  		if block == nil {
   567  			break
   568  		}
   569  		if block.Type != "CERTIFICATE" || len(block.Headers) != 0 {
   570  			continue
   571  		}
   572  
   573  		certBytes := block.Bytes
   574  		cert, err := x509.ParseCertificate(certBytes)
   575  		if err != nil {
   576  			fmt.Printf("%v\n", err)
   577  			continue
   578  		}
   579  		ss = append(ss, model.CertString(cert))
   580  	}
   581  
   582  	sort.Strings(ss)
   583  	for i, s := range ss {
   584  		fmt.Printf("%03d:\n%s\n", i+1, s)
   585  	}
   586  
   587  	return len(ss), nil
   588  }
   589  
   590  func listP7C(fName string) (int, error) {
   591  	bb, err := os.ReadFile(fName)
   592  	if err != nil {
   593  		fmt.Printf("%v\n", err)
   594  		return 0, err
   595  	}
   596  
   597  	if len(bb) == 0 {
   598  		//return 0, errors.Errorf("%s is empty\n", filepath.Base(fName))
   599  		return 0, errors.New("is empty\n")
   600  	}
   601  
   602  	// // Check if the data starts with PEM markers (for Base64 encoding)
   603  	// if isPEM(data) {
   604  	// 	// If the file is Base64 encoded (PEM format), decode it
   605  	// 	decodedData, err := base64.StdEncoding.DecodeString(string(data))
   606  	// 	if err != nil {
   607  	// 		log.Fatalf("Error decoding Base64: %v", err)
   608  	// 	}
   609  	// 	data = decodedData
   610  	// }
   611  
   612  	p7, err := pkcs7.Parse(bb)
   613  	if err != nil {
   614  		return 0, err
   615  	}
   616  
   617  	ss := []string{}
   618  	for _, cert := range p7.Certificates {
   619  		ss = append(ss, model.CertString(cert))
   620  	}
   621  
   622  	sort.Strings(ss)
   623  	for i, s := range ss {
   624  		fmt.Printf("%03d:\n%s\n", i+1, s)
   625  	}
   626  
   627  	return len(ss), nil
   628  }
   629  
   630  // ListCertificatesAll returns formatted information about installed certificates.
   631  func ListCertificatesAll(json bool, conf *model.Configuration) ([]string, error) {
   632  	// Process *.pem and *.p7c
   633  	fmt.Printf("certDir: %s\n", model.CertDir)
   634  
   635  	if err := os.MkdirAll(model.CertDir, os.ModePerm); err != nil {
   636  		return nil, err
   637  	}
   638  
   639  	count := 0
   640  
   641  	err := filepath.WalkDir(model.CertDir, func(path string, d os.DirEntry, err error) error {
   642  		if err != nil {
   643  			return err
   644  		}
   645  		if d.IsDir() {
   646  			return nil
   647  		}
   648  		if !model.IsPEM(path) && !model.IsP7C(path) {
   649  			return nil
   650  		}
   651  
   652  		fmt.Printf("\n%s:\n", strings.TrimPrefix(path, model.CertDir))
   653  
   654  		if model.IsPEM(path) {
   655  			c, err := listPEM(path)
   656  			if err != nil {
   657  				fmt.Printf("%v\n", err)
   658  			}
   659  			count += c
   660  			return nil
   661  		}
   662  		c, err := listP7C(path)
   663  		if err != nil {
   664  			fmt.Printf("%v\n", err)
   665  		}
   666  		count += c
   667  		return nil
   668  	})
   669  
   670  	fmt.Printf("total installed certs: %d\n", count)
   671  
   672  	return nil, err
   673  }