]> Sergey Matveev's repositories - public-inbox.git/blob - lib/PublicInbox/GitCatFile.pm
git cat-file wrapper uses syswrite for writes
[public-inbox.git] / lib / PublicInbox / GitCatFile.pm
1 # Copyright (C) 2014, Eric Wong <normalperson@yhbt.net> and all contributors
2 # License: GPLv2 or later (https://www.gnu.org/licenses/gpl-2.0.txt)
3 # This is based on code in Git.pm which is GPLv2, but modified to avoid
4 # dependence on environment variables for compatibility with mod_perl.
5 # There are also API changes to simplify our usage and data set.
6 package PublicInbox::GitCatFile;
7 use strict;
8 use warnings;
9 use Fcntl qw(F_GETFD F_SETFD FD_CLOEXEC);
10 use POSIX qw(dup2);
11
12 sub new {
13         my ($class, $git_dir) = @_;
14         bless { git_dir => $git_dir }, $class;
15 }
16
17 sub set_cloexec {
18         my ($fh) = @_;
19         my $flags = fcntl($fh, F_GETFD, 0) or die "fcntl(F_GETFD): $!\n";
20         fcntl($fh, F_SETFD, $flags | FD_CLOEXEC) or die "fcntl(F_SETFD): $!\n";
21 }
22
23 sub _cat_file_begin {
24         my ($self) = @_;
25         return if $self->{pid};
26         my ($in_r, $in_w, $out_r, $out_w);
27
28         pipe($in_r, $in_w) or die "pipe failed: $!\n";
29         set_cloexec($_) foreach ($in_r, $in_w);
30         pipe($out_r, $out_w) or die "pipe failed: $!\n";
31         set_cloexec($_) foreach ($out_r, $out_w);
32
33         my @cmd = ('git', "--git-dir=$self->{git_dir}", qw(cat-file --batch));
34         my $pid = fork;
35         defined $pid or die "fork failed: $!\n";
36         if ($pid == 0) {
37                 dup2(fileno($out_r), 0) or die "redirect stdin failed: $!\n";
38                 dup2(fileno($in_w), 1) or die "redirect stdout failed: $!\n";
39                 exec(@cmd) or die 'exec `' . join(' '). "' failed: $!\n";
40         }
41         close $out_r or die "close failed: $!\n";
42         close $in_w or die "close failed: $!\n";
43
44         $self->{in} = $in_r;
45         $self->{out} = $out_w;
46         $self->{pid} = $pid;
47 }
48
49 sub cat_file {
50         my ($self, $object) = @_;
51
52         $object .= "\n";
53         my $len = bytes::length($object);
54
55         $self->_cat_file_begin;
56         my $written = syswrite($self->{out}, $object);
57         if (!defined $written) {
58                 die "pipe write error: $!\n";
59         } elsif ($written != $len) {
60                 die "wrote too little to pipe ($written < $len)\n";
61         }
62
63         my $in = $self->{in};
64         my $head = <$in>;
65         $head =~ / missing$/ and return undef;
66         $head =~ /^[0-9a-f]{40} \S+ (\d+)$/ or
67                 die "Unexpected result from git cat-file: $head\n";
68
69         my $size = $1;
70         my $bytes_left = $size;
71         my $buf;
72         my $rv = '';
73
74         while ($bytes_left) {
75                 my $read = read($in, $buf, $bytes_left);
76                 defined($read) or die "read pipe failed: $!\n";
77                 $rv .= $buf;
78                 $bytes_left -= $read;
79         }
80
81         my $read = read($in, $buf, 1);
82         defined($read) or die "read pipe failed: $!\n";
83         if ($read != 1 || $buf ne "\n") {
84                 die "newline missing after blob\n";
85         }
86         \$rv;
87 }
88
89 sub DESTROY {
90         my ($self) = @_;
91         my $pid = $self->{pid} or return;
92         $self->{pid} = undef;
93         foreach my $f (qw(in out)) {
94                 my $fh = $self->{$f};
95                 defined $fh or next;
96                 close $fh;
97                 $self->{$f} = undef;
98         }
99         waitpid $pid, 0;
100 }
101
102 1;