File Coverage

lib/CodeGen/Protection.pm
Criterion Covered Total %
statement 32 32 100.0
branch n/a
condition n/a
subroutine 11 11 100.0
pod 1 2 50.0
total 44 45 97.7


line stmt bran cond sub pod time code
1             package CodeGen::Protection;
2              
3             # ABSTRACT: Safely rewrite parts of generated code
4              
5 3     3   396897 use v5.10.0; # for named captures in regexes
  3         41  
6 3     3   15 use strict;
  3         5  
  3         62  
7 3     3   11 use warnings;
  3         5  
  3         90  
8 3     3   28 use base 'Exporter';
  3         5  
  3         337  
9 3     3   2087 use Module::Runtime qw( use_module );
  3         5938  
  3         27  
10 3     3   237 use Carp 'croak';
  3         4  
  3         179  
11 3         53 use CodeGen::Protection::Types qw(
12             compile_named
13             NonEmptyStr
14             Bool
15             Optional
16 3     3   1200 );
  3         10  
17              
18             our $VERSION = '0.01';
19             our @EXPORT_OK = qw(
20             create_protected_code
21             rewrite_code
22             );
23             our %EXPORT_TAGS = ( all => \@EXPORT_OK );
24              
25             sub create_protected_code {
26 3     3 1 299 state $check = compile_named(
27             type => NonEmptyStr,
28             protected_code => NonEmptyStr,
29             tidy => Optional [Bool],
30             name => Optional [NonEmptyStr],
31             overwrite => Optional [Bool],
32             );
33 3         12600 return _rewritten( $check->(@_) );
34             }
35              
36             sub rewrite_code {
37 9     9 0 25105 state $check = compile_named(
38             type => NonEmptyStr,
39             protected_code => NonEmptyStr,
40             existing_code => NonEmptyStr,
41             tidy => Optional [Bool],
42             name => Optional [NonEmptyStr],
43             overwrite => Optional [Bool],
44             );
45 9         11191 return _rewritten( $check->(@_) );
46             }
47              
48             sub _rewritten {
49 12     12   580 my $arg_for = shift;
50 12         38 my $type = delete $arg_for->{type};
51 12         42 my $class = _use_module($type);
52 12         316 return $class->new($arg_for)->rewritten;
53             }
54              
55             sub _use_module {
56 12     12   26 my $type = shift;
57 12         47 my $class = "CodeGen::Protection::Format::$type";
58 12         73 use_module($class);
59 12         440 return $class;
60             }
61              
62             1;
63              
64             __END__
65              
66             =pod
67              
68             =encoding UTF-8
69              
70             =head1 NAME
71              
72             CodeGen::Protection - Safely rewrite parts of generated code
73              
74             =head1 VERSION
75              
76             version 0.01
77              
78             =head1 SYNOPSIS
79              
80             use CodeGen::Protection qw(:all);
81              
82             # Creating a new document:
83              
84             my $perl = create_protected_code(
85             type => 'Perl',
86             protected_code => $sample,
87             );
88              
89             # Or rewriting:
90              
91             my $rewritten = rewrite_code(
92             type => 'Perl',
93             existing_code => $perl,
94             protected_code => $rewritten_code,
95             );
96              
97             =head1 DESCRIPTION
98              
99             Code that writes code can be a powerful tool, especially when you need to
100             generate lots of boilerplate. However, when a developer takes the generated
101             code, they can easily rewrite that code in a way that no longer works, or make
102             good changes that get wiped out if the code is regenerated.
103             L<https://metacpan.org/pod/DBIx::Class::Schema::Loader> protects against this
104             by marking blocks of code with start and end comments and an MD5 checksum. If
105             you change any of the code between those comments, regenerating your schema
106             will fail.
107              
108             This module takes this idea and generalizes it. It allows you to do a safe
109             partial rewrite of documents. At the present time, we support Perl and HTML.
110              
111             In short, we wrap your "protected" (C<protected_code>) code in start and end
112             comments, with checksums for the code:
113              
114             #<<< CodeGen::Protection::Perl 0.01. Do not touch any code between this and the end comment. Checksum: fa97a021bd70bf3b9fa3e52f203f2660
115            
116             # protected code goes here
117              
118             #>>> CodeGen::Protection::Perl 0.01. Do not touch any code between this and the start comment. Checksum: fa97a021bd70bf3b9fa3e52f203f2660
119              
120             Or:
121              
122             <!-- CodeGen::Protection::Format::HTML 0.01. Do not touch any code between this and the end comment. Checksum: c286b9b2577e085df857227eae996c40 -->
123            
124             <ol>
125             <li>This is a list</li>
126             <li>This is the second entry.</li>
127             </ol>
128            
129             <!-- CodeGen::Protection::Format::HTML 0.01. Do not touch any code between this and the start comment. Checksum: c286b9b2577e085df857227eae996c40 -->
130              
131             If calling the C<rewrite_code> function, this module removes the code between
132             the C<existing_code>'s start and end markers and replaces it with the
133             C<protected_code>. If the code between the start and end markers has been
134             altered, it will no longer match the checksums and rewriting the code will
135             fail.
136              
137             =head1 TYPES
138              
139             As of this writing, we can protect Perl and HTML:
140              
141             my $rewritten = rewrite_code(
142             type => 'Perl',
143             existing_code => $perl,
144             protected_code => $protected_code,
145             );
146              
147             my $rewritten = rewrite_code(
148             type => 'HTML',
149             existing_code => $HTML,
150             protected_code => $protected_code,
151             );
152              
153             See L<CodeGen::Protection::Role> to learn how to create your own types to protect.
154              
155             =head1 FUNCTIONS
156              
157             Functions are exportable on-demand, or both can be exported via C<:all>.
158              
159             use CodeGen::Protection qw(rewrite_code);
160             use CodeGen::Protection qw(:all);
161              
162             =head2 C<create_protected_code>
163              
164             my $protected_code = create_protected_code(
165             type => 'Perl',
166             protected_code => $text_of_code,
167             );
168              
169             =head3 ARGUMENTS
170              
171             Both C<create_protected_code> and C<rewrite_code> take the same arguments,
172             except that C<rewrite_code> does not allow the C<protected_code> argument.
173              
174             =over 4
175              
176             =item * C<protected_code>
177              
178             This is a required string containing any new Perl code to be built with this
179             tool.
180              
181             =item * C<existing_code>
182              
183             This is an optional string containing Perl code already built with this tool.
184             If provided, this code I<must> have the start and end markers generated by
185             this tool so that the rewriter knows the section of code to replace with the
186             injected code.
187              
188             =item * C<name>
189              
190             Optional name for the code. This is only used in error messages if you're
191             generating a lot of code and an error occurs and you'd like to see the name
192             in the error.
193              
194             =item * C<tidy>
195              
196             If true, will attempt to tidy the C<protected_code> block (the rest of the
197             code is ignored). For Perl, if the value of perltidy is the number 1 (one),
198             then a generic pass of L<Perl::Tidy> will be done on the code. If the value is
199             true and anything I<other> than one, this is assumed to be the path to a
200             F<.perltidyrc> file and that will be used to tidy the code (or C<croak()> if
201             the F<.perltidyrc> file cannot be found).
202              
203             =item * C<overwrite>
204              
205             Optional boolean, default false. In "Rewrite mode", if the checksum in the
206             start and end markers doesn't match the code within them, someone has manually
207             altered that code and we do not automatically overwrite it (in fact, we
208             C<croak()>). Setting C<overwrite> to true will cause it to be overwritten.
209              
210             =back
211              
212             =head1 MODES
213              
214             There are two modes: "Creation" and "Rewrite."
215              
216             =head2 Creation Mode
217              
218             my $rewrite = CodeGen::Protection::Perl->new(
219             protected_code => $text,
220             );
221             say $rewrite->rewritten;
222              
223             If you create an instance with C<protected_code> but not old text, this will wrap
224             the new text in start and end tags that "protect" the document if you rewrite
225             it:
226              
227             my $perl = <<'END';
228             sub sum {
229             my $total = 0;
230             $total += $_ foreach @_;
231             return $total;
232             }
233             END
234             my $rewrite = CodeGen::Protection::Perl->new( protected_code => $perl );
235             say $rewrite->rewritten;
236              
237             Output:
238              
239             #<<< CodeGen::Protection::Perl 0.01. Do not touch any code between this and the end comment. Checksum: fa97a021bd70bf3b9fa3e52f203f2660
240              
241             sub sum {
242             my $total = 0;
243             $total += $_ foreach @_;
244             return $total;
245             }
246              
247             #>>> CodeGen::Protection::Perl 0.01. Do not touch any code between this and the start comment. Checksum: fa97a021bd70bf3b9fa3e52f203f2660
248              
249             You can then take the marked up document and insert it into another Perl
250             document and use the rewrite mode to safely rewrite the code between the start
251             and end markers. The rest of the document will be ignored.
252              
253             Note that leading and trailing comments start with C<< #<<< >> and C<< #>>> >>
254             respectively. Those are special comments which tell L<Perl::Tidy> to ignore
255             what ever is between them. Thus, you can safely tidy code written with this.
256              
257             The start and end checksums are the same and are the checksum of the text
258             between the comments. Leading and trailing lines which are all whitespace are
259             removed and one leading and one trailing newline will be added.
260              
261             =head2 Rewrite Mode
262              
263             Given a document created with the "Creating" mode, you can then take the
264             marked up document and insert it into another Perl document and use the
265             rewrite mode to safely rewrite the code between the start and end markers.
266             The rest of the document will be ignored.
267              
268             my $rewrite = CodeGen::Protection::Perl->new(
269             existing_code => $existing_code,
270             protected_code => $protected_code,
271             );
272             say $rewrite->rewritten;
273              
274             In the above, assuming that C<$existing_code> is a rewritable document, the
275             C<$protected_code> will replace the rewritable section of the C<$existing_code>, leaving
276             the rest unchanged.
277              
278             However, if C<$protected_code> is I<also> a rewritable document, then the rewritable
279             portion of the C<$protected_code> will be extract and used to replace the rewritable
280             portion of the C<$existing_code>.
281              
282             So for the code shown in the "Creation mode" section, you could add more code
283             like this:
284              
285             package My::Package;
286              
287             use strict;
288             use warnings;
289              
290             sub average {
291             return sum(@_)/@_;
292             }
293              
294             #<<< CodeGen::Protection::Perl 0.01. Do not touch any code between this and the end comment. Checksum: fa97a021bd70bf3b9fa3e52f203f2660
295              
296             sub sum {
297             my $total = 0;
298             $total += $_ foreach @_;
299             return $total;
300             }
301              
302             #>>> CodeGen::Protection::Perl 0.01. Do not touch any code between this and the start comment. Checksum: fa97a021bd70bf3b9fa3e52f203f2660
303            
304             1;
305              
306             However, later on I might realize that the C<sum> function will happily try to
307             sum things which are not numbers, so I want to fix that. I'll slurp the C<My::Package> code
308             into the C<$existing_code> variable and then:
309              
310             my $perl = <<'END';
311             use Scalar::Util 'looks_like_number';
312              
313             sub sum {
314             my $total = 0;
315             foreach my $number (@_) {
316             unless (looks_like_number($number)) {
317             die "'$number' doesn't look like a numbeer!";
318             }
319             $total += $number;
320             }
321             return $total;
322             }
323             END
324             my $rewrite = CodeGen::Protection::Perl->new( existing_code => $existing_code, protected_code => $perl );
325             say $rewrite->rewritten;
326              
327             And that will print out:
328              
329             package My::Package;
330            
331             use strict;
332             use warnings;
333            
334             sub average {
335             return sum(@_)/@_;
336             }
337            
338             #<<< CodeGen::Protection::Perl 0.01. Do not touch any code between this and the end comment. Checksum: d135a051f158ee19fbd68af5466fb1ae
339            
340             use Scalar::Util 'looks_like_number';
341            
342             sub sum {
343             my $total = 0;
344             foreach my $number (@_) {
345             unless (looks_like_number($number)) {
346             die "'$number' doesn't look like a numbeer!";
347             }
348             $total += $number;
349             }
350             return $total;
351             }
352            
353             #>>> CodeGen::Protection::Perl 0.01. Do not touch any code between this and the start comment. Checksum: d135a051f158ee19fbd68af5466fb1ae
354            
355             1;
356              
357             You can see that the code between the start and end checksum comments and been
358             rewritten, while the rest of the code remains unchanged.
359              
360             =head1 ACKNOWLEDGEMENTS
361              
362             We would like to thank L<All Around the World|https://allaroundtheworld.fr/>
363             for sponsoring this work.
364              
365             =head1 AUTHOR
366              
367             Curtis "Ovid" Poe <ovid@allaroundtheworld.fr>
368              
369             =head1 COPYRIGHT AND LICENSE
370              
371             This software is copyright (c) 2021 by Curtis "Ovid" Poe.
372              
373             This is free software; you can redistribute it and/or modify it under
374             the same terms as the Perl 5 programming language system itself.
375              
376             =cut