Module: Pikuri::Workspace::Search::Utils

Defined in:
lib/pikuri/workspace/search/utils.rb

Overview

The two guards every path-listing tool needs and none should own: an oversized-root refusal, and a byte cap on the rendered result.

Class Method Summary collapse

Class Method Details

.denied_globs(filesystem:, root:) ⇒ Array<String>

rg --glob '!<rel>' for each denied root under root, pruning the subtree before rg ever walks it. Verified: a negative glob on a directory prunes its contents and survives --no-ignore, the same property Glob's !.git/* relies on.

This is the optimization, never the guarantee — it goes silently partial whenever a denied root sits outside the search root, and an rg flag change could weaken it without a test noticing. The Filesystem#denied? filter on the results is what fails closed.

Parameters:

  • filesystem (Filesystem)
  • root (Pathname)

    the rg search root

Returns:

  • (Array<String>)

    argv fragments, empty when nothing is denied



134
135
136
137
# File 'lib/pikuri/workspace/search/utils.rb', line 134

def self.denied_globs(filesystem:, root:)
  denied_prefixes(filesystem: filesystem, root: root)
    .flat_map { |rel| ['--glob', "!#{rel}"] }
end

.denied_prefixes(filesystem:, root:) ⇒ Array<String>

Search-root-relative paths of every denied root that falls under root — the only ones an rg walk rooted there could reach.

denied_prefixes(filesystem: fs, root: Pathname.new('/home/alice'))
# => [".aws", ".gnupg", ".ssh"]

Parameters:

Returns:

  • (Array<String>)

    sorted, +""+-free relative paths



111
112
113
114
115
116
117
118
119
# File 'lib/pikuri/workspace/search/utils.rb', line 111

def self.denied_prefixes(filesystem:, root:)
  base = normalize_root(root)
  filesystem.denied_roots.filter_map { |denied|
    absolute = denied.exist? ? denied.realpath.to_s : denied.to_s
    next unless absolute.start_with?(base + File::SEPARATOR)

    absolute.delete_prefix(base + File::SEPARATOR)
  }.sort
end

.head_truncate(raw, max_bytes:, hint:) ⇒ Array(String, String)

Head-truncate raw to max_bytes, cutting at the last newline so the final row is never partial.

Parameters:

  • raw (String)
  • max_bytes (Integer)
  • hint (String)

    what the caller should change to get less output, e.g. "refine pattern or path"

Returns:

  • (Array(String, String))

    the content and a marker (+""+ when nothing was cut)



89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/pikuri/workspace/search/utils.rb', line 89

def self.head_truncate(raw, max_bytes:, hint:)
  total = raw.bytesize
  return [raw, ''] if total <= max_bytes

  head = raw.byteslice(0, max_bytes)
  last_nl = head.rindex("\n")
  head = head.byteslice(0, last_nl) if last_nl
  omitted = total - head.bytesize
  marker = "\n\n... [#{omitted} bytes omitted; total was #{total} bytes; " \
           "#{hint}] ..."
  [head, marker]
end

.normalize_root(path) ⇒ String

Returns symlink-resolved absolute path; falls back to a lexical cleanup when the path doesn't exist on disk.

Parameters:

  • path (String, Pathname)

Returns:

  • (String)

    symlink-resolved absolute path; falls back to a lexical cleanup when the path doesn't exist on disk.



74
75
76
77
78
# File 'lib/pikuri/workspace/search/utils.rb', line 74

def self.normalize_root(path)
  Pathname.new(path).realpath.to_s
rescue SystemCallError
  Pathname.new(path).expand_path.cleanpath.to_s
end

.oversized_root_refusal(resolved, filesystem:, verb:) ⇒ String?

Guard against a search/list rooted at /, the user's whole home dir, or any parent of home (+/home+, which holds every user's home) — each walks an enormous tree and, under an AllowAll workspace, floods rg with unreadable-path diagnostics the model pays context for. The refused set is home and everything at-or-above it: blocking ~ while waving through its strictly-broader parent /home would be no guard at all. Refused regardless of any glob: rg traverses the tree before the glob narrows which files it searches. Lesser roots (a project dir, ~/work) return nil and proceed.

Rather than dead-end at "narrow it", the refusal hands back a one-level listing of the root's own entries, so the model can pick a subdirectory and retry without another tool call or a shell:

Error: refusing to search from /home/alice — the whole home
directory is too broad. Pick one of its entries below as `path` and
search there instead (directories end with `/`):
.config/  [3 dirs, 2 files]
.ssh/  [denied]
work/  [12 dirs, 1 file]
todo.txt  412B

The listing is deliberately one level deep: rendering a tree would re-do the very traversal this guard exists to prevent. (The child summary does open each entry once, but that's bounded by the root's own entry count — nothing like rg's walk.) It falls back to a plain "narrow path" line if the directory is empty or unreadable.

Parameters:

  • resolved (Pathname)

    the absolute search root

  • filesystem (Filesystem)

    threaded into the listing so a credential dir under the refused root renders [denied]

  • verb (String)

    the tool's action word for the message, e.g. "search" (Grep) or "list" (Glob)

Returns:

  • (String, nil)

    the "Error: ..." refusal, or nil to proceed



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/pikuri/workspace/search/utils.rb', line 43

def self.oversized_root_refusal(resolved, filesystem:, verb:)
  normalized = normalize_root(resolved)
  home = normalize_root(Pathname.new(Dir.home))
  root_kind =
    if normalized == normalize_root(Pathname.new('/'))
      'the whole filesystem root'
    elsif normalized == home
      'the whole home directory'
    elsif home.start_with?(normalized + File::SEPARATOR)
      # e.g. /home — a parent of ~ is strictly broader than ~ itself.
      # (+/+ is a parent too, but its "//"-prefix test never matches,
      # so it stays owned by the first branch and its clearer message.)
      'a parent of your home directory'
    end
  return nil unless root_kind

  listing = root_listing(normalized, filesystem: filesystem)
  guidance =
    if listing.empty?
      'Narrow `path` to a specific subdirectory.'
    else
      "Pick one of its entries below as `path` and #{verb} there " \
        "instead (directories end with `/`):\n#{listing}"
    end
  "Error: refusing to #{verb} from #{resolved}#{root_kind} is too " \
    "broad. #{guidance}"
end

.reject_denied_lines(raw, prefixes:) ⇒ String

Drop every output line rg emitted for a denied path. rg puts the path first on the line and never follows symlinks (no -L in either tool's argv), so a leading-prefix test is exact for the path segment — no need to find where the path ends, which a filename containing : would make ambiguous. It may over-match (a .netrc.bak line goes too); over-filtering is the safe direction for a credential guard.

Parameters:

  • raw (String)

    rg output, ./ prefixes already stripped

  • prefixes (Array<String>)

Returns:

  • (String)


149
150
151
152
153
154
155
# File 'lib/pikuri/workspace/search/utils.rb', line 149

def self.reject_denied_lines(raw, prefixes:)
  return raw if prefixes.empty?

  raw.lines.reject { |line|
    prefixes.any? { |rel| line.start_with?(rel) }
  }.join
end